Merge tag 'kivitendo_3.5.4-2' into b-3.5.6.1
authorMichael Wagner <michael@wagnertech.de>
Sat, 16 Jan 2021 21:27:00 +0000 (22:27 +0100)
committerMichael Wagner <michael@wagnertech.de>
Sat, 16 Jan 2021 21:27:00 +0000 (22:27 +0100)
477 files changed:
.htaccess
SL/AM.pm
SL/AP.pm
SL/AR.pm
SL/ArchiveZipFixes.pm [deleted file]
SL/Auth.pm
SL/Auth/ColumnInformation.pm
SL/Auth/LDAP.pm
SL/BackgroundJob/CreatePeriodicInvoices.pm
SL/BackgroundJob/MassDeliveryOrderPrinting.pm
SL/BackgroundJob/MassRecordCreationAndPrinting.pm
SL/BackgroundJob/SelfTest.pm
SL/BackgroundJob/SelfTest/Transactions.pm
SL/BackgroundJob/SetNumberRange.pm [new file with mode: 0644]
SL/BackgroundJob/Test.pm
SL/CA.pm
SL/CT.pm
SL/ClientJS.pm
SL/Common.pm
SL/Controller/BankTransaction.pm
SL/Controller/Base.pm
SL/Controller/ClientConfig.pm
SL/Controller/CsvImport.pm
SL/Controller/CsvImport/ARTransaction.pm
SL/Controller/CsvImport/BankTransaction.pm
SL/Controller/CsvImport/Base.pm
SL/Controller/CsvImport/BaseMulti.pm
SL/Controller/CsvImport/DeliveryOrder.pm [new file with mode: 0644]
SL/Controller/CustomerVendor.pm
SL/Controller/CustomerVendorTurnover.pm
SL/Controller/DeliveryPlan.pm
SL/Controller/Inventory.pm
SL/Controller/Letter.pm
SL/Controller/LoginScreen.pm
SL/Controller/MassInvoiceCreatePrint.pm
SL/Controller/Order.pm
SL/Controller/Part.pm
SL/Controller/PriceSource.pm
SL/Controller/Project.pm
SL/Controller/Reconciliation.pm
SL/Controller/RequirementSpec.pm
SL/Controller/SalesPurchase.pm
SL/Controller/SimpleSystemSetting.pm
SL/Controller/YearEndTransactions.pm
SL/Controller/ZUGFeRD.pm [new file with mode: 0644]
SL/DATEV.pm
SL/DATEV/CSV.pm
SL/DB.pm
SL/DB/ContactDepartment.pm [new file with mode: 0644]
SL/DB/ContactTitle.pm [new file with mode: 0644]
SL/DB/CsvImportReport.pm
SL/DB/Customer.pm
SL/DB/Default.pm
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/Attr.pm
SL/DB/Helper/AttrSorted.pm
SL/DB/Helper/FlattenToForm.pm
SL/DB/Helper/Mappings.pm
SL/DB/Helper/PDF_A.pm [new file with mode: 0644]
SL/DB/Helper/Payment.pm
SL/DB/Helper/PriceTaxCalculator.pm
SL/DB/Helper/VATIDNrValidation.pm [new file with mode: 0644]
SL/DB/Helper/ZUGFeRD.pm [new file with mode: 0644]
SL/DB/Invoice.pm
SL/DB/Manager/BackgroundJob.pm
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/Greeting.pm [new file with mode: 0644]
SL/DB/Manager/Part.pm
SL/DB/Manager/PriceRule.pm
SL/DB/Manager/ReconciliationLink.pm
SL/DB/MetaSetup/Assembly.pm
SL/DB/MetaSetup/AssortmentItem.pm
SL/DB/MetaSetup/BackgroundJob.pm
SL/DB/MetaSetup/BankAccount.pm
SL/DB/MetaSetup/BankTransactionAccTrans.pm
SL/DB/MetaSetup/Business.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/Customer.pm
SL/DB/MetaSetup/Default.pm
SL/DB/MetaSetup/DeliveryOrderItem.pm
SL/DB/MetaSetup/DunningConfig.pm
SL/DB/MetaSetup/GLTransaction.pm
SL/DB/MetaSetup/Greeting.pm [new file with mode: 0644]
SL/DB/MetaSetup/InvoiceItem.pm
SL/DB/MetaSetup/Order.pm
SL/DB/MetaSetup/OrderItem.pm
SL/DB/MetaSetup/PaymentTerm.pm
SL/DB/MetaSetup/Tax.pm
SL/DB/MetaSetup/Vendor.pm
SL/DB/Object.pm
SL/DB/Order.pm
SL/DB/OrderItem.pm
SL/DB/Part.pm
SL/DB/PeriodicInvoicesConfig.pm
SL/DB/PurchaseInvoice.pm
SL/DB/Shipto.pm
SL/DB/ShopOrder.pm
SL/DB/Unit.pm
SL/DB/Vendor.pm
SL/DBUpgrade2.pm
SL/DBUtils.pm
SL/DN.pm
SL/DO.pm
SL/Dev/CustomerVendor.pm
SL/Dev/Part.pm
SL/Dev/Payment.pm
SL/Dev/Record.pm
SL/Dispatcher.pm
SL/Dispatcher/AuthHandler/User.pm
SL/File.pm
SL/File/Backend/Webdav.pm
SL/File/Object.pm
SL/Form.pm
SL/GL.pm
SL/Helper/CreatePDF.pm
SL/Helper/ISO3166.pm [new file with mode: 0644]
SL/Helper/ISO4217.pm [new file with mode: 0644]
SL/Helper/MT940.pm
SL/Helper/MassPrintCreatePDF.pm
SL/Helper/UNECERecommendation20.pm [new file with mode: 0644]
SL/Helper/UserPreferences.pm
SL/Helper/UserPreferences/PartPickerSearch.pm [new file with mode: 0644]
SL/Helper/UserPreferences/UpdatePositions.pm [new file with mode: 0644]
SL/IC.pm
SL/IR.pm
SL/IS.pm
SL/InstallationCheck.pm
SL/InstanceConfiguration.pm
SL/LXDebug.pm
SL/Mailer.pm
SL/OE.pm
SL/Presenter/Part.pm
SL/Presenter/Tag.pm
SL/ReportGenerator.pm
SL/Request.pm
SL/SEPA.pm
SL/System/TaskServer.pm
SL/Taxkeys.pm
SL/Template.pm
SL/Template/LaTeX.pm
SL/Template/Plugin/L.pm
SL/Template/XML.pm [deleted file]
SL/USTVA.pm
SL/VATIDNr.pm [new file with mode: 0644]
SL/VK.pm
SL/WH.pm
SL/Webdav.pm
SL/X.pm
SL/ZUGFeRD.pm [new file with mode: 0644]
VERSION
bin/mozilla/am.pl
bin/mozilla/ap.pl
bin/mozilla/ar.pl
bin/mozilla/dn.pl
bin/mozilla/do.pl
bin/mozilla/generictranslations.pl
bin/mozilla/gl.pl
bin/mozilla/io.pl
bin/mozilla/ir.pl
bin/mozilla/is.pl
bin/mozilla/login.pl
bin/mozilla/oe.pl
bin/mozilla/sepa.pl
bin/mozilla/ustva.pl
bin/mozilla/vk.pl
bin/mozilla/wh.pl
config/kivitendo.conf.default
css/kivitendo/main.css
css/lx-office-erp/main.css
doc/UPGRADE
doc/changelog
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
doc/html/ch02s20.html
doc/html/ch02s21.html
doc/html/ch03.html
doc/html/ch03s02.html
doc/html/ch03s03.html
doc/html/ch03s04.html
doc/html/ch03s05.html
doc/html/ch03s06.html
doc/html/ch03s07.html
doc/html/ch03s08.html
doc/html/ch03s09.html
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
doc/html/index.html
doc/kivitendo-Dokumentation.pdf
image/kivitendo_corona.png [new file with mode: 0644]
image/rotate_cw.svg [new file with mode: 0644]
js/client_js.js
js/kivi.ActionBar.js
js/kivi.CustomerVendor.js
js/kivi.GL.js
js/kivi.MassInvoiceCreatePrint.js
js/kivi.Order.js
js/kivi.Part.js
js/kivi.SalesPurchase.js
js/kivi.js
js/locale/de.js
js/locale/en.js
js/requirement_spec.js
locale/de/all
locale/de/special_chars
locale/en/all
locale/en/special_chars
menus/user/00-erp.yaml
modules/override/Algorithm/CheckDigits/M97_001.pm [new file with mode: 0644]
scripts/dbupgrade2_tool.pl
scripts/find-use.pl
scripts/image_maps.pl
scripts/installation_check.pl
scripts/task_server.pl
sql/Pg-upgrade2-auth/all_drafts_edit.pl
sql/Pg-upgrade2-auth/master_rights_positions_fix.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/purchase_letter_rights.pl
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/right_purchase_all_edit.sql [new file with mode: 0644]
sql/Pg-upgrade2-auth/rights_sales_purchase_edit_prices.sql [new file with mode: 0644]
sql/Pg-upgrade2/acc_trans_without_oid.sql
sql/Pg-upgrade2/add_node_id_to_background_jobs.sql [new file with mode: 0644]
sql/Pg-upgrade2/alter_default_shipped_qty.sql [new file with mode: 0644]
sql/Pg-upgrade2/ap_set_payment_term_from_vendor.sql [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_transaction_acc_trans_remove_wrong_primary_key.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/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/customer_create_zugferd_invoices.sql [new file with mode: 0644]
sql/Pg-upgrade2/customer_vendor_add_natural_person.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_zugferd_data.sql [new file with mode: 0644]
sql/Pg-upgrade2/defaults_split_address.pl [new file with mode: 0644]
sql/Pg-upgrade2/defaults_vc_greetings_use_textfield.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/delivery_orders.sql
sql/Pg-upgrade2/dunning_config_print_original_invoice.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/exchangerate_in_oe.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_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/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/periodic_invoices_background_job.pl [deleted file]
sql/Pg-upgrade2/periodic_invoices_background_job.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/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_taxkey_15_17_skr04.sql [new file with mode: 0644]
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/tax_removed_taxnumber.sql [new file with mode: 0644]
sql/Pg-upgrade2/transfer_out_serial_charge_number.sql [new file with mode: 0644]
sql/lx-office.sql
t/000setup_database.t
t/006spellcheck.t
t/ar/ar.t
t/bank/bank_transactions.t
t/bank/cb_ob_transactions.t [deleted file]
t/controllers/csvimport/artransactions.t
t/datev/datev_format_2018.t
t/datev/invoices.t
t/db_helper/convert_invoice.t
t/db_helper/payment.t
t/db_helper/price_tax_calculator.t
t/gl/gl.t [new file with mode: 0644]
t/helper/attr.t
t/shop/shop_order.t
t/structure/instance_conf_method_names.t
t/tax/tax.t [new file with mode: 0644]
t/year_end/year_end.t [new file with mode: 0644]
templates/mail/self_test/status_mail.txt
templates/pdf/pdf_a_metadata.xmp [new file with mode: 0644]
templates/print/RB/credit_note.tex
templates/print/RB/deutsch.tex
templates/print/RB/english.tex
templates/print/RB/invoice.tex
templates/print/RB/proforma.tex
templates/print/RB/purchase_order.tex
templates/print/RB/request_quotation.tex
templates/print/RB/sales_delivery_order.tex
templates/print/RB/sales_order.tex
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/firma/Briefkopf.png [new file with mode: 0644]
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/default_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/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/zahlungserinnerung.tex [new file with mode: 0644]
templates/print/marei/zahlungserinnerung_invoice.tex [new file with mode: 0644]
templates/webpages/am/config.html
templates/webpages/am/list_account_details.html
templates/webpages/am/list_tax.html
templates/webpages/ap/form_header.html
templates/webpages/ap/search.html
templates/webpages/ar/ar_transactions_bottom.html
templates/webpages/ar/form_header.html
templates/webpages/client_config/_default_accounts.html
templates/webpages/client_config/_features.html
templates/webpages/client_config/_miscellaneous.html
templates/webpages/client_config/_warehouse.html
templates/webpages/common/_ship_to_dialog.html
templates/webpages/common/render_cvar_input.html
templates/webpages/csv_import/_form_delivery_orders.html [new file with mode: 0644]
templates/webpages/csv_import/form.html
templates/webpages/csv_import/report.html
templates/webpages/customer_vendor/form.html
templates/webpages/customer_vendor/tabs/billing.html
templates/webpages/customer_vendor/tabs/contacts.html
templates/webpages/customer_vendor/tabs/price_list.html [new file with mode: 0644]
templates/webpages/customer_vendor/tabs/vcnotes.html
templates/webpages/customer_vendor_turnover/_list_open_items.html
templates/webpages/customer_vendor_turnover/_list_open_orders.html
templates/webpages/customer_vendor_turnover/count_turnover.html
templates/webpages/customer_vendor_turnover/invoices_statistic.html
templates/webpages/customer_vendor_turnover/order_statistic.html
templates/webpages/customer_vendor_turnover/quotation_statistic.html
templates/webpages/do/form_footer.html
templates/webpages/do/form_header.html
templates/webpages/dunning/add.html
templates/webpages/dunning/edit_config.html
templates/webpages/dunning/show_invoices.html
templates/webpages/generic/calculate_qty.html
templates/webpages/generictranslations/edit_zugferd_notes.html [new file with mode: 0644]
templates/webpages/gl/form_header.html
templates/webpages/gl/update_tax_accounts.html
templates/webpages/gl/yearend_bottom.html [deleted file]
templates/webpages/gl/yearend_filter.html [deleted file]
templates/webpages/gl/yearend_top.html [deleted file]
templates/webpages/ic/search.html
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_header.html
templates/webpages/login_screen/user_login.html
templates/webpages/mass_invoice_create_print_from_do/_filter.html
templates/webpages/mass_invoice_create_print_from_do/list_invoices.html
templates/webpages/oe/form_header.html
templates/webpages/oe/search.html
templates/webpages/order/form.html
templates/webpages/order/tabs/_item_input.html
templates/webpages/order/tabs/_multi_items_dialog.html
templates/webpages/order/tabs/_price_sources_dialog.html
templates/webpages/order/tabs/_row.html
templates/webpages/order/tabs/basic_data.html
templates/webpages/part/_basic_data.html
templates/webpages/part/_customerprices.html
templates/webpages/part/_edit_translations.html
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
templates/webpages/part/_multi_items_dialog.html
templates/webpages/part/form.html
templates/webpages/part/part_picker_search.html
templates/webpages/part/test_page.html
templates/webpages/simple_system_setting/_bank_account_form.html
templates/webpages/ustva/generic_taxreport.html [deleted file]
templates/webpages/ustva/ustva.html
templates/webpages/vk/search_invoice.html
templates/webpages/wh/journal_filter.html
templates/webpages/wh/report_filter.html
templates/webpages/wh/warehouse_selection.html
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]

index 977b220..106aeb4 100644 (file)
--- a/.htaccess
+++ b/.htaccess
@@ -1,19 +1,16 @@
-### Choose a character set (just in case you like to change it here)
-### uncomment 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 <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"
-### examples for Apache <= 2.2: "Allow from 192.168" or "Allow from 192.168.1" or "Allow from 192.168.178" or "Allow from 217.84.201.2"
 #<IfModule mod_authz_core.c>
 #  # Apache 2.4
 #  Require ip 192.168
 #</IfModule>
-#<IfModule !mod_authz_core.c>
-#  # Apache 2.2
-#  Order deny,allow
-#  Deny from all
-#  Allow from 192.168
-#</IfModule>
+
+<IfModule mod_rewrite.c>
+  RewriteEngine On
+  RewriteRule .*/(\.git|config)/.*$ - [F,NC]
+</IfModule>
+
index a981017..b7c6003 100644 (file)
--- a/SL/AM.pm
+++ b/SL/AM.pm
@@ -53,6 +53,8 @@ use SL::DB::Vendor;
 use SL::DB;
 use SL::GenericTranslations;
 use SL::Helper::UserPreferences::PositionsScrollbar;
+use SL::Helper::UserPreferences::PartPickerSearch;
+use SL::Helper::UserPreferences::UpdatePositions;
 
 use strict;
 
@@ -104,7 +106,7 @@ sub get_account {
 
     # get the taxkeys of the account
     $form->{ACCOUNT_TAXKEYS} = [];
-    foreach my $taxkey ( @{ $chart_obj->taxkeys } ) {
+    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,
@@ -532,6 +534,18 @@ 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 save_preferences {
   $main::lxdebug->enter_sub();
 
@@ -560,6 +574,15 @@ sub save_preferences {
   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})
+  }
 
   $main::lxdebug->leave_sub();
 
@@ -1009,13 +1032,16 @@ sub taxes {
                    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);
@@ -1156,14 +1182,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         = ?
@@ -1177,13 +1202,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);
index caae2bb..937af8b 100644 (file)
--- a/SL/AP.pm
+++ b/SL/AP.pm
@@ -41,10 +41,12 @@ use SL::IO;
 use SL::MoreCommon;
 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 {
@@ -137,15 +139,15 @@ sub _post_transaction {
 
     $query = qq|UPDATE ap SET invnumber = ?,
                 transdate = ?, ordnumber = ?, vendor_id = ?, taxincluded = ?,
-                amount = ?, duedate = ?, paid = ?, netamount = ?,
+                amount = ?, duedate = ?, deliverydate = ?, paid = ?, netamount = ?,
                 currency_id = (SELECT id FROM currencies WHERE name = ?), notes = ?, department_id = ?, storno = ?, storno_id = ?,
                 globalproject_id = ?, direct_debit = ?
                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}),
+                  $form->{invpaid}, $form->{netamount},
                   $form->{currency}, $form->{notes},
                   conv_i($form->{department_id}), $form->{storno},
                   $form->{storno_id}, conv_i($form->{globalproject_id}),
@@ -155,6 +157,31 @@ sub _post_transaction {
 
     $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) {
@@ -427,6 +454,7 @@ sub ap_transactions {
     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
@@ -440,7 +468,8 @@ sub ap_transactions {
     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 = '';
 
@@ -449,7 +478,8 @@ sub ap_transactions {
   # 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.
-  # - Filter by employee if requested.
+  # - Limit to own invoices unless may edit all invoices.
+  # - If may edit all, allow filtering by employee.
   my (@permission_where, @permission_values);
 
   if ($::auth->assert('vendor_invoice_edit', 1)) {
@@ -457,9 +487,16 @@ sub ap_transactions {
       push @permission_where, "NOT invoice = 'f'"; # remove ap transactions from Purchase -> Reports -> Invoices
     }
 
-    if ($form->{employee_id}) {
+    if (!$::auth->assert('purchase_all_edit', 1)) {
+      # only show own invoices
       push @permission_where,  "a.employee_id = ?";
-      push @permission_values, conv_i($form->{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});
+      }
     }
   }
 
@@ -519,6 +556,14 @@ sub ap_transactions {
     $where .= " AND a.transdate <= ?";
     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}) {
       $where .= " AND a.amount <> a.paid" if ($form->{open});
@@ -563,7 +608,7 @@ SQL
   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))) {
     $sortorder = $form->{sort} . " $sortdir";
   }
 
@@ -848,7 +893,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));
@@ -868,7 +913,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;
index 188bf9e..b26911a 100644 (file)
--- a/SL/AR.pm
+++ b/SL/AR.pm
@@ -134,14 +134,14 @@ sub _post_transaction {
     $query =
       qq|UPDATE ar set
            invnumber = ?, ordnumber = ?, transdate = ?, customer_id = ?,
-           taxincluded = ?, amount = ?, duedate = ?, paid = ?,
+           taxincluded = ?, amount = ?, duedate = ?, deliverydate = ?, paid = ?,
            currency_id = (SELECT id FROM currencies WHERE name = ?),
            netamount = ?, notes = ?, department_id = ?,
            employee_id = ?, storno = ?, storno_id = ?, globalproject_id = ?,
            direct_debit = ?
          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},
+                  conv_date($form->{duedate}), conv_date($form->{deliverydate}), $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},
@@ -682,7 +682,7 @@ SQL
   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 name datepaid employee shippingpoint shipvia transaction_description department))) {
     $sortorder = $form->{sort} . " $sortdir";
   }
 
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 9f52a37..82513b9 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);
 
@@ -72,7 +72,7 @@ sub reset {
     delete $self->{column_information};
   }
 
-  $self->{authenticator}->reset;
+  $_->reset for @{ $self->{authenticators} };
 
   $self->client(undef);
 }
@@ -118,7 +118,10 @@ 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";
@@ -140,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};
@@ -167,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});
@@ -227,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;
 }
@@ -412,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 {
@@ -1095,6 +1126,8 @@ sub evaluate_rights_ary {
   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;
@@ -1204,6 +1237,8 @@ sub assert {
   }
 
   if (!$dont_abort) {
+    $::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."));
   }
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 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 521dc1b..5c223e6 100644 (file)
@@ -9,6 +9,7 @@ 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;
@@ -18,6 +19,7 @@ use SL::DB::PeriodicInvoicesConfig;
 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
@@ -329,6 +331,26 @@ sub _send_summary_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 _print_invoice {
   my ($self, $data) = @_;
 
@@ -387,6 +409,7 @@ sub _email_invoice {
     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',
@@ -403,6 +426,8 @@ sub _email_invoice {
   eval {
     $pdf_file_name = $self->create_pdf(%create_params);
 
+    $self->_store_pdf_in_webdav($pdf_file_name, $data->{invoice});
+
     for (qw(email_subject email_body)) {
       _replace_vars(
         object           => $data->{config},
index c46a8d7..772d566 100644 (file)
@@ -12,7 +12,7 @@ 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);
+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;
@@ -102,4 +102,3 @@ sub run {
 }
 
 1;
-
index e134807..0ab03bd 100644 (file)
@@ -14,8 +14,7 @@ use SL::Template;
 use SL::Locale::String qw(t8);
 use SL::Helper::MassPrintCreatePDF qw(:all);
 use SL::Helper::CreatePDF qw(:all);
-use SL::Helper::File qw(store_pdf append_general_pdf_attachments);
-use SL::Webdav;
+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;
@@ -50,17 +49,12 @@ 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);
 
-      if (!$db->with_transaction(sub {
-        $invoice = $sales_delivery_order->convert_to_invoice(sub { $data->{transdate} ? ('attributes' => { transdate => $data->{transdate} }) :
-                                                                         undef }->() ) || die $db->error;
-        1;
-      })) {
-        die $db->error;
-      }
+      die $db->error if !$invoice;
 
       $data->{num_created}++;
       push @{ $data->{invoice_ids} }, $invoice->id;
@@ -104,32 +98,18 @@ sub convert_invoices_to_pdf {
   foreach my $invoice (@{ $self->{invoices} }) {
 
     eval {
+      my @errors = ();
       my %params = (
         variables => \%variables,
         return    => 'file_name',
         document  => $invoice,
+        errors    => \@errors,
       );
       push @pdf_file_names, $self->create_massprint_pdf(%params);
-
       $data->{num_printed}++;
 
-      # OLD WebDAV Code, may be deleted:
-      # 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 => $@ };
-        }
+      if (scalar @errors) {
+        push @{ $data->{print_errors} }, { id => $invoice->id, number => $invoice->invnumber, message => join(', ', @errors) };
       }
 
       1;
index d341e64..b715f9e 100644 (file)
@@ -26,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),
   ],
 );
 
@@ -88,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 {
@@ -103,7 +106,7 @@ sub run_module {
   } 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;
@@ -188,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 3fbc0d1..9eda722 100644 (file)
@@ -15,7 +15,7 @@ sub run {
 
   $self->_setup;
 
-  $self->tester->plan(tests => 32);
+  $self->tester->plan(tests => 34);
 
   $self->check_konten_mit_saldo_nicht_in_guv;
   $self->check_bilanzkonten_mit_pos_eur;
@@ -208,22 +208,35 @@ sub check_invnumbers_unique {
 sub check_summe_stornobuchungen {
   my ($self) = @_;
 
-  my $query = qq|
-    SELECT sum(amount) from ar a WHERE a.id IN
-      (SELECT id from ap 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 WHERE a.id IN
-      (SELECT id from ap 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 Verkaufsrechnungen (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 {
@@ -631,7 +644,7 @@ sub check_orphaned_reconciliated_links {
   my $query = qq|
           SELECT purpose from bank_transactions
           WHERE cleared is true
-          AND id not in (SELECT bank_transaction_id from reconciliation_links)
+          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);
@@ -681,7 +694,7 @@ sub check_orphaned_bank_transaction_acc_trans_links {
   my $query = qq|
           SELECT purpose from bank_transactions
           WHERE invoice_amount <> 0
-          AND id not in (SELECT bank_transaction_id from bank_transaction_acc_trans)
+          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 <= ?|;
 
@@ -701,7 +714,7 @@ sub check_orphaned_bank_transaction_acc_trans_links {
           SELECT purpose from bank_transactions
           WHERE id in
           (SELECT bank_transaction_id from bank_transaction_acc_trans
-           where acc_trans_id NOT IN (select acc_trans_id from 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);
@@ -801,10 +814,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>
@@ -812,4 +821,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;
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 d954482..019d44f 100644 (file)
--- a/SL/CA.pm
+++ b/SL/CA.pm
@@ -101,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
@@ -113,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,
@@ -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);
 
index 6a52ee7..0c20a6f 100644 (file)
--- a/SL/CT.pm
+++ b/SL/CT.pm
@@ -84,6 +84,9 @@ sub search {
       "salesman"           => "e.name",
       "payment"            => "pt.description",
       "pricegroup"         => "pg.pricegroup",
+      "ustid"              => "ct.ustid",
+      "creditlimit"        => "ct.creditlimit",
+      "commercial_court"   => "ct.commercial_court",
     );
 
   $form->{sort} ||= "name";
@@ -98,7 +101,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}";
index a134cad..336f971 100644 (file)
@@ -113,6 +113,7 @@ 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>)
index b70378e..fb1b4b8 100644 (file)
@@ -539,16 +539,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);
@@ -562,7 +562,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 ) ) ) {
@@ -590,9 +594,11 @@ sub copy_file_to_webdav_folder {
 
   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();
 }
 
index 844dad5..dc66ef0 100644 (file)
@@ -68,32 +68,21 @@ sub action_list_all {
   $self->report_generator_list_objects(report => $self->{report}, objects => $self->models->get);
 }
 
-sub action_list {
-  my ($self) = @_;
-
-  if (!$::form->{filter}{bank_account}) {
-    flash('error', t8('No bank account chosen!'));
-    $self->action_search;
-    return;
-  }
+sub gather_bank_transactions_and_proposals {
+  my ($self, %params) = @_;
 
-  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 => { ge => $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(
@@ -102,7 +91,7 @@ sub action_list {
     limit        => 10000,
     where        => [
       amount                => {ne => \'invoice_amount'},
-      local_bank_account_id => $::form->{filter}{bank_account},
+      local_bank_account_id => $params{bank_account}->id,
       cleared               => 0,
       @where
     ],
@@ -117,7 +106,7 @@ sub action_list {
                                                                        with_objects => ['customer','payment_terms']);
 
   my $all_open_ap_invoices = SL::DB::Manager::PurchaseInvoice->get_all(where => [amount => { ne => \'paid' }], with_objects => ['vendor'  ,'payment_terms']);
-  my $all_open_sepa_export_items = SL::DB::Manager::SepaExportItem->get_all(where => [chart_id => $bank_account->chart_id ,
+  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;
@@ -229,26 +218,43 @@ sub action_list {
   push @proposals, @otherproposals;
 
   # sort bank transaction proposals by quality (score) of proposal
-  if ($::form->{sort_by} && $::form->{sort_by} eq 'proposal') {
-    if ($::form->{sort_dir}) {
-      $bank_transactions = [ sort { $a->{agreement} <=> $b->{agreement} } @{ $bank_transactions } ];
-    } else {
-      $bank_transactions = [ sort { $b->{agreement} <=> $a->{agreement} } @{ $bank_transactions } ];
-    }
+  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 } ];
   }
 
-  # for testing with t/bank/banktransaction.t :
-  if ( $::form->{dont_render_for_test} ) {
-    return ( $bank_transactions , \@proposals );
+  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,
+                PROPOSALS         => $proposals,
                 bank_account      => $bank_account,
-                ui_tab            => scalar(@proposals) > 0?1:0,
+                ui_tab            => scalar(@{ $proposals }) > 0 ? 1 : 0,
               );
 }
 
index 5847b98..5e0fd26 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;
@@ -54,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);
 }
@@ -74,6 +75,7 @@ sub render {
     header     => 1,
     layout     => 1,
     process    => 1,
+    status     => '200 ok',
   );
   $options->{$_} //= $defaults{$_} for keys %defaults;
   $options->{type} = lc $options->{type};
@@ -130,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});
     }
   }
 
@@ -156,16 +159,22 @@ 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;
index 59fe4f5..9bf0eda 100644 (file)
@@ -18,7 +18,9 @@ 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;
 
 __PACKAGE__->run_before('check_auth');
 
@@ -98,6 +100,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) {
@@ -188,11 +195,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 {
index 0a6935e..bb1e8d6 100644 (file)
@@ -20,13 +20,14 @@ 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);
@@ -163,7 +164,21 @@ sub action_report {
     $::form->error(t8('No report with id #1', $report_id));
   }
 
-  my $num_rows               = $self->{report}->numrows;
+  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;
@@ -180,31 +195,35 @@ sub action_report {
   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
-  ];
+
+
+  $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);
@@ -288,7 +307,7 @@ sub 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 ar_transactions);
+  die "Invalid CSV import type" if none { $_ eq $::form->{profile}->{type} } qw(parts inventories customers_vendors addresses contacts projects orders delivery_orders bank_transactions ar_transactions);
   $self->type($::form->{profile}->{type});
 }
 
@@ -335,11 +354,12 @@ sub render_inputs {
             : $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' or $self->{type} eq 'ar_transactions' ) {
+  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 ]));
   };
 
@@ -488,7 +508,7 @@ 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});
   }
 
@@ -703,6 +723,7 @@ sub init_worker {
        : $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";
index 4018b78..36760fe 100644 (file)
@@ -133,7 +133,7 @@ sub setup_displayable_columns {
                                  { name => 'projectnumber', description => $::locale->text('Project (number)')       },
                                  { name => 'project',       description => $::locale->text('Project (description)')  },
                                  { name => 'amount',        description => $::locale->text('Amount')                 },
-                                 { name => 'chart',         description => $::locale->text('Account number')         },
+                                 { name => 'accno',         description => $::locale->text('Account number')         },
                                  { name => 'taxkey',        description => $::locale->text('Taxkey')                 },
                                 );
 }
@@ -160,14 +160,17 @@ sub check_objects {
 
   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 ) {
-      $self->handle_transaction($entry);
+      die "Cannot process transaction row without an invoice row" if !$invoice_entry;
+      $self->handle_transaction($entry, $invoice_entry);
     } else {
       die "unknown datatype";
     };
@@ -288,24 +291,23 @@ sub handle_invoice {
 }
 
 sub check_taxkey {
-  my ($self, $entry, $chart) = @_;
+  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 $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;
-
-  if ( $entry->{raw_data}->{taxkey} == $chart->get_active_taxkey->tax->taxkey ) {
-    $tax = $chart->get_active_taxkey->tax;
-  } else {
+  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} ]);
   };
@@ -340,7 +342,7 @@ sub check_amounts {
 };
 
 sub handle_transaction {
-  my ($self, $entry) = @_;
+  my ($self, $entry, $invoice_entry) = @_;
 
   # Prepare acc_trans data. amount is dealt with in add_transactions_to_ar
 
@@ -355,7 +357,7 @@ sub handle_transaction {
       return 0;
     };
 
-    if ( $self->check_taxkey($entry, $chart_obj) ) {
+    if ( $self->check_taxkey($entry, $invoice_entry, $chart_obj) ) {
       # do nothing, taxkey was assigned, just continue
     } else {
       # missing taxkey, don't do anything
index db22be0..8b5b5e7 100644 (file)
@@ -175,20 +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},
-                         $entry->{raw_data}->{purpose12},
-                         $entry->{raw_data}->{purpose13} );
+  my $purpose =
+    join ' ',
+    grep { ($_ // '') !~ m{^ *$} }
+    map  { $entry->{raw_data}->{"purpose$_"} }
+    ('', 1..13);
+
   $object->purpose($purpose);
 
 }
index 9e8956f..e3c119d 100644 (file)
@@ -551,6 +551,7 @@ sub save_objects {
           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);
         }
       }
@@ -592,14 +593,25 @@ 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_history {
   my ($self, $object) = @_;
 
-  if (any { $self->controller->{type} && $_ eq $self->controller->{type} } qw(parts customers_vendors orders ar_transactions)) {
+  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
                  : '';
 
@@ -607,6 +619,9 @@ sub _save_history {
     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 b975394..5168ae2 100644 (file)
@@ -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) = @_;
 
diff --git a/SL/Controller/CsvImport/DeliveryOrder.pm b/SL/Controller/CsvImport/DeliveryOrder.pm
new file mode 100644 (file)
index 0000000..62ecb11
--- /dev/null
@@ -0,0 +1,1226 @@
+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::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::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 => 'is_sales',                description => $::locale->text('Is sales')                              },
+                                 { name => 'language',                description => $::locale->text('Language (name)')                       },
+                                 { name => 'language_id',             description => $::locale->text('Language (database ID)')                },
+                                 { name => 'notes',                   description => $::locale->text('Notes')                                 },
+                                 { 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_is_sales($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_is_sales {
+  my ($self, $entry) = @_;
+
+  if (!exists $entry->{raw_data}->{is_sales}) {
+    $entry->{object}->is_sales(!!$entry->{object}->customer_id);
+  }
+}
+
+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');
+  }
+
+  $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} };
+
+  foreach my $order (@{ $record->{source_orders} }) {
+    $item->{source_item} = first { $item->parts_id == $_->parts_id && $item->qty == $_->qty} @{ $order->items_sorted };
+    last if $item->{source_item};
+  }
+}
+
+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)) {
+    foreach my $order (@{ $orders }) {
+      my $all_deliverd;
+      foreach my $orderitem (@{ $order->items }) {
+        my $delivered_qty = 0;
+        foreach my $do_item (@{$orderitem->linked_records(to => 'DeliveryOrderItem')}) {
+          $delivered_qty += $do_item->unit_obj->convert_to($do_item->qty, $orderitem->unit_obj);
+        }
+        $all_deliverd = $orderitem->qty <= $delivered_qty;
+        last if !$all_deliverd;
+      }
+      $order->update_attributes(delivered => !!$all_deliverd);
+    }
+  }
+
+  # 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 c8fd5fd..8b10f2c 100644 (file)
@@ -9,6 +9,8 @@ use SL::JSON;
 use SL::DBUtils;
 use SL::Helper::Flash;
 use SL::Locale::String;
+use SL::Util qw(trim);
+use SL::Webdav;
 use SL::Controller::Helper::GetModels;
 use SL::Controller::Helper::ReportGenerator;
 use SL::Controller::Helper::ParseFilter;
@@ -16,12 +18,16 @@ 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;
@@ -69,6 +75,7 @@ __PACKAGE__->run_before(
     'update',
     'ajaj_get_shipto',
     'ajaj_get_contact',
+    'ajax_list_prices',
   ]
 );
 
@@ -159,6 +166,21 @@ sub _save {
     $::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->with_transaction(sub {
@@ -184,8 +206,13 @@ sub _save {
 
     $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);
     }
 
@@ -477,7 +504,8 @@ sub action_search_contact {
 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();
 
@@ -619,8 +647,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;
@@ -660,6 +688,62 @@ sub action_test_page {
   $_[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';
 }
@@ -863,33 +947,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();
 
@@ -927,18 +1002,6 @@ sub _pre_render {
     $self->{all_pricegroups} = SL::DB::Manager::Pricegroup->get_all_sorted(query => [ or => [ id => $self->{cv}->pricegroup_id, obsolete => 0 ] ]);
   }
 
-  $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)
-    )
-  ];
-
   $self->{contacts} = $self->{cv}->contacts;
   $self->{contacts} ||= [];
 
@@ -984,6 +1047,21 @@ sub _pre_render {
       ],
     );
   }
+
+  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('kivi.CustomerVendor.js');
index f0047e5..5a59f1f 100644 (file)
@@ -27,6 +27,7 @@ sub action_list_turnover {
                                          amount => { lt => \'paid'},
                                        ],
                       ],
+      sort_by      => 'transdate DESC',
       with_objects => [ 'dunnings' ],
     );
   } else {
@@ -38,12 +39,11 @@ sub action_list_turnover {
                                   amount => { lt => \'paid'},
                                 ],
                  ],
-      sort_by => 'invnumber DESC',
+      sort_by => 'transdate DESC',
     );
   }
   my $open_items;
   if (@{$open_invoices}) {
-    return $self->render(\'', { type => 'json' }) unless scalar @{$open_invoices};
     $open_items = $self->_list_open_items($open_invoices);
   }
   my $open_orders = $self->_get_open_orders;
@@ -181,12 +181,12 @@ sub action_get_invoices {
   if ( $::form->{db} eq 'customer' ) {
     $invoices = SL::DB::Manager::Invoice->get_all(
       query   => [ customer_id => $cv, ],
-      sort_by => 'invnumber DESC',
+      sort_by => 'transdate DESC',
     );
   } else {
     $invoices = SL::DB::Manager::PurchaseInvoice->get_all(
       query   => [ vendor_id => $cv, ],
-      sort_by => 'invnumber DESC',
+      sort_by => 'transdate DESC',
     );
   }
   $self->render('customer_vendor_turnover/invoices_statistic', { layout => 0 }, invoices => $invoices);
@@ -206,7 +206,7 @@ sub action_get_orders {
                    customer_id => $cv,
                    quotation   => ($type eq 'quotation' ? 'T' : 'F')
                  ],
-      sort_by => ( $type eq 'order' ? 'ordnumber DESC' : 'quonumber DESC'),
+      sort_by => 'transdate DESC',
     );
   } else {
     $orders = SL::DB::Manager::Order->get_all(
@@ -214,7 +214,7 @@ sub action_get_orders {
                    vendor_id => $cv,
                    quotation => ($type eq 'quotation' ? 'T' : 'F')
                  ],
-      sort_by => ( $type eq 'order' ? 'ordnumber DESC' : 'quonumber DESC'),
+      sort_by => 'transdate DESC',
     );
   }
   if ( $type eq 'order') {
@@ -237,7 +237,7 @@ sub _get_open_orders {
                    customer_id => $cv,
                    closed      => 'F',
                  ],
-      sort_by => 'ordnumber DESC',
+      sort_by => 'transdate DESC',
     );
   } else {
     $open_orders = SL::DB::Manager::Order->get_all(
@@ -245,7 +245,7 @@ sub _get_open_orders {
                    vendor_id => $cv,
                    closed    => 'F',
                  ],
-      sort_by => 'ordnumber DESC',
+      sort_by => 'transdate DESC',
     );
   }
 
index f8d5d03..4a45863 100644 (file)
@@ -338,7 +338,7 @@ sub init_models {
 }
 
 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";
index f20f6ae..30ff321 100644 (file)
@@ -20,6 +20,7 @@ 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);
 
@@ -44,6 +45,12 @@ 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');
   my $transfer_types = WH->retrieve_transfer_types('in');
   map { $_->{description} = $main::locale->text($_->{description}) } @{ $transfer_types };
@@ -781,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;
 }
@@ -913,7 +953,7 @@ sub _already_counted {
 
   my %bestbefore_filter;
   if ($::instance_conf->get_show_bestbefore) {
-    %bestbefore_filter = (bestbefore => $params{bestbefore});
+    %bestbefore_filter = (bestbefore => ($params{bestbefore} || undef));
   }
 
   SL::DB::Manager::Stocktaking->get_all(query => [and => [parts_id     => $part->id,
index beb1a46..3ee9eb7 100644 (file)
@@ -106,7 +106,7 @@ sub action_update_contacts {
     return $self->js
       ->replaceWith(
         '#letter_cp_id',
-        SL::Presenter->get->select_tag('letter.cp_id', [], value_key => 'cp_id', title_key => 'full_name')
+        select_tag('letter.cp_id', [], value_key => 'cp_id', title_key => 'full_name')
       )
       ->render;
   }
@@ -151,7 +151,7 @@ sub action_delete {
   my ($self, %params) = @_;
 
   if (!$self->letter->delete) {
-    flash('error', t8('An error occured. Letter could not be deleted.'));
+    flash('error', t8('An error occurred. Letter could not be deleted.'));
     return $self->action_update;
   }
 
index 673c6b4..59fabb2 100644 (file)
@@ -58,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;
+  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}));
 
@@ -179,7 +181,7 @@ sub init_default_client_id {
 sub show_login_form {
   my ($self, %params) = @_;
 
-  $self->render('login_screen/user_login', %params, version => SL::Version->get_version );
+  $self->render('login_screen/user_login', %params, version => SL::Version->get_version, callback => $::form->{callback});
 }
 
 1;
index a3cf2a0..aab6373 100644 (file)
@@ -22,7 +22,7 @@ use SL::SessionFile;
 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');
@@ -113,7 +113,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 {
@@ -262,6 +262,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');
@@ -291,7 +295,7 @@ sub download_or_print_documents {
       });
 
     @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}) {
index f6f6986..bef3631 100644 (file)
@@ -4,12 +4,13 @@ use strict;
 use parent qw(SL::Controller::Base);
 
 use SL::Helper::Flash qw(flash_later);
-use SL::Presenter::Tag qw(select_tag hidden_tag);
+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::Order;
@@ -20,15 +21,17 @@ use SL::DB::PartsGroup;
 use SL::DB::Printer;
 use SL::DB::Language;
 use SL::DB::RecordLink;
+use SL::DB::Shipto;
 
 use SL::Helper::CreatePDF qw(:all);
 use SL::Helper::PrintOptions;
 use SL::Helper::ShippedQty;
 use SL::Helper::UserPreferences::PositionsScrollbar;
+use SL::Helper::UserPreferences::UpdatePositions;
 
 use SL::Controller::Helper::GetModels;
 
-use List::Util qw(first);
+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);
@@ -38,8 +41,8 @@ use Sort::Naturally;
 
 use Rose::Object::MakeMethods::Generic
 (
- scalar => [ qw(item_ids_to_delete) ],
- 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors) ],
+ scalar => [ qw(item_ids_to_delete is_custom_shipto_to_delete) ],
+ 'scalar --get_set_init' => [ qw(order valid_types type cv p multi_items_models all_price_factors search_cvpartnumber show_update_button) ],
 );
 
 
@@ -47,10 +50,12 @@ use Rose::Object::MakeMethods::Generic
 __PACKAGE__->run_before('check_auth');
 
 __PACKAGE__->run_before('recalc',
-                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice print send_email) ]);
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_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 print send_email) ]);
+                        only => [ qw(save save_as_new save_and_delivery_order save_and_invoice save_and_ap_transaction
+                                     print send_email) ]);
 
 #
 # actions
@@ -238,11 +243,8 @@ sub action_save_as_new {
 # print the order
 #
 # This is called if "print" is pressed in the print dialog.
-# If PDF creation was requested and succeeded, the pdf is stored in a session
-# file and the filename is stored as session value with an unique key. A
-# javascript function with this key is then called. This function calls the
-# download action below (action_download_pdf), which offers the file for
-# download.
+# 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) = @_;
 
@@ -294,16 +296,13 @@ sub action_print {
 
   if ($media eq 'screen') {
     # screen/download
-    my $sfile = SL::SessionFile::Random->new(mode => "w");
-    $sfile->fh->print($pdf);
-    $sfile->fh->close;
-
-    my $key = join('_', Time::HiRes::gettimeofday(), int rand 1000000000000);
-    $::auth->set_session_value("Order::print-${key}" => $sfile->file_name);
-
-    $self->js
-    ->run('kivi.Order.download_pdf', $pdf_filename, $key)
-    ->flash('info', t8('The PDF has been created'));
+    $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
@@ -350,24 +349,16 @@ sub action_print {
   $self->js->render;
 }
 
-# offer pdf for download
-#
-# It needs to get the key for the session value to get the pdf file.
-sub action_download_pdf {
+# open the email dialog
+sub action_save_and_show_email_dialog {
   my ($self) = @_;
 
-  my $key = $::form->{key};
-  my $tmp_filename = $::auth->get_session_value("Order::print-${key}");
-  return $self->send_file(
-    $tmp_filename,
-    type => 'application/pdf',
-    name => $::form->{pdf_filename},
-  );
-}
+  my $errors = $self->save();
 
-# open the email dialog
-sub action_show_email_dialog {
-  my ($self) = @_;
+  if (scalar @{ $errors }) {
+    $self->js->flash('error', $_) foreach @{ $errors };
+    return $self->js->render();
+  }
 
   my $cv_method = $self->cv;
 
@@ -385,9 +376,11 @@ sub action_show_email_dialog {
 
   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}         = 'de';
+  $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';
 
   $email_form->{subject}             = $form->generate_email_subject();
@@ -459,6 +452,7 @@ sub action_send_email {
     $::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
@@ -474,11 +468,15 @@ sub action_send_email {
 
   $self->order->update_attributes(intnotes => $intnotes);
 
-  $self->js
-      ->val('#order_intnotes', $intnotes)
-      ->run('kivi.Order.close_email_dialog')
-      ->flash('info', t8('The email has been sent.'))
-      ->render($self);
+  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
@@ -647,6 +645,33 @@ 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) = @_;
+
+  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 = (
+    controller => 'ap.pl',
+    action     => 'add_from_purchase_order',
+    id         => $self->order->id,
+  );
+
+  $self->redirect_to(@redirect_params);
+}
+
 # set form elements in respect to a changed customer or vendor
 #
 # This action is called on an change of the customer/vendor picker.
@@ -665,9 +690,9 @@ sub action_customer_vendor_changed {
   }
 
   if ($self->order->$cv_method->shipto && scalar @{ $self->order->$cv_method->shipto } > 0) {
-    $self->js->show('#shipto_row');
+    $self->js->show('#shipto_selection');
   } else {
-    $self->js->hide('#shipto_row');
+    $self->js->hide('#shipto_selection');
   }
 
   $self->js->val( '#order_salesman_id',      $self->order->salesman_id)        if $self->order->is_sales;
@@ -675,16 +700,20 @@ sub action_customer_vendor_changed {
   $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(        '#language_id',            $self->order->$cv_method->language_id)
-    ->focus(      '#order_' . $self->cv . '_id');
+    ->focus(      '#order_' . $self->cv . '_id')
+    ->run('kivi.Order.update_exchangerate');
 
   $self->js_redisplay_amounts_and_taxes;
+  $self->js_redisplay_cvpartnumbers;
   $self->js->render();
 }
 
@@ -755,16 +784,22 @@ sub action_add_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,
-                                     TYPE              => $self->type,
-                                     ALL_PRICE_FACTORS => $self->all_price_factors
+                                     ITEM => $item,
+                                     ID   => $item_id,
+                                     SELF => $self,
   );
 
-  $self->js
-    ->append('#row_table_id', $row_as_html);
+  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};
@@ -781,25 +816,31 @@ sub action_add_item {
 
       $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,
-                                         TYPE              => $self->type,
-                                         ALL_PRICE_FACTORS => $self->all_price_factors
+                                         ITEM => $item,
+                                         ID   => $item_id,
+                                         SELF => $self,
       );
-      $self->js
-        ->append('#row_table_id', $row_as_html);
+      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.Order.init_row_handlers')
-    ->run('kivi.Order.row_table_scroll_down')
     ->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();
 }
@@ -862,24 +903,31 @@ sub action_add_multi_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,
-                                       TYPE              => $self->type,
-                                       ALL_PRICE_FACTORS => $self->all_price_factors
+                                       ITEM => $item,
+                                       ID   => $item_id,
+                                       SELF => $self,
     );
 
-    $self->js->append('#row_table_id', $row_as_html);
+    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.Order.close_multi_items_dialog')
     ->run('kivi.Order.init_row_handlers')
-    ->run('kivi.Order.row_table_scroll_down')
     ->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();
 }
@@ -895,18 +943,33 @@ sub action_recalc_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 },
+    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}) {
@@ -974,6 +1037,55 @@ sub action_load_second_rows {
   $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];
+
+    $item->description($item->part->description);
+    $item->longdescription($item->part->notes);
+
+    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.Order.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->recalc();
+  $self->js_redisplay_line_values;
+  $self->js_redisplay_amounts_and_taxes;
+
+  $self->js->render();
+}
+
 sub js_load_second_row {
   my ($self, $item, $item_id, $do_parse) = @_;
 
@@ -1054,6 +1166,17 @@ sub js_redisplay_amounts_and_taxes {
     ->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) = @_;
 
@@ -1104,6 +1227,23 @@ sub init_cv {
   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;
 }
@@ -1166,15 +1306,31 @@ sub build_contact_select {
 sub build_shipto_select {
   my ($self) = @_;
 
-  select_tag('order.shipto_id', [ $self->order->{$self->cv}->shipto ],
-    value_key  => 'shipto_id',
-    title_key  => 'displayable_id',
-    default    => $self->order->shipto_id,
-    with_empty => 1,
-    style      => 'width: 300px',
+  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.
@@ -1224,6 +1380,12 @@ sub load_order {
   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
@@ -1240,8 +1402,9 @@ sub make_order {
   # 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())));
+  $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}) {
@@ -1249,11 +1412,13 @@ sub make_order {
     setup_order_from_cv($order);
   }
 
-  my $form_orderitems               = delete $::form->{order}->{orderitems};
-  my $form_periodic_invoices_config = delete $::form->{order}->{periodic_invoices_config};
+  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);
@@ -1339,8 +1504,9 @@ sub new_item {
     $price_src->price($item->sellprice);
   } else {
     $price_src = $price_source->best_price
-           ? $price_source->best_price
-           : $price_source->price_from_source("");
+               ? $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;
   }
 
@@ -1381,7 +1547,7 @@ sub new_item {
 sub setup_order_from_cv {
   my ($order) = @_;
 
-  $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id));
+  $order->$_($order->customervendor->$_) for (qw(taxzone_id payment_id delivery_term_id currency_id));
 
   $order->intnotes($order->customervendor->notes);
 
@@ -1394,26 +1560,46 @@ sub setup_order_from_cv {
 
 }
 
+# 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) = @_;
 
-  # bb: todo: currency later
-  $self->order->currency_id($::instance_conf->get_currency_id());
-
   my %pat = $self->order->calculate_prices_and_taxes();
+
   $self->{taxes} = [];
-  foreach my $tax_chart_id (keys %{ $pat{taxes} }) {
-    my $tax = SL::DB::Manager::Tax->find_by(chart_id => $tax_chart_id);
+  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} };
 
-    my @amount_keys = grep { $pat{amounts}->{$_}->{tax_id} == $tax->id } keys %{ $pat{amounts} };
-    push(@{ $self->{taxes} }, { amount    => $pat{taxes}->{$tax_chart_id},
-                                netamount => $pat{amounts}->{$amount_keys[0]}->{amount},
-                                tax       => $tax });
+    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}};
 }
 
@@ -1465,6 +1651,12 @@ sub save {
   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::OrderItem->new(id => $_)->delete for @{$self->item_ids_to_delete || []};
     $self->order->save(cascade => 1);
 
@@ -1513,6 +1705,14 @@ sub workflow_sales_or_purchase_order {
                        : $::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};
 
@@ -1521,6 +1721,15 @@ sub workflow_sales_or_purchase_order {
     $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);
@@ -1549,6 +1758,7 @@ 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_employees}              = SL::DB::Manager::Employee->get_all(where => [ or => [ id => $self->order->employee_id,
                                                                                               deleted => 0 ] ],
@@ -1565,10 +1775,11 @@ sub pre_render {
   $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;
-  $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
-  $self->{print_options}   = SL::Helper::PrintOptions->get_print_options(
+  $print_form->{type}        = $self->type;
+  $print_form->{printers}    = SL::DB::Manager::Printer->get_all_sorted;
+  $print_form->{languages}   = SL::DB::Manager::Language->get_all_sorted;
+  $print_form->{language_id} = $self->order->language_id;
+  $self->{print_options}     = SL::Helper::PrintOptions->get_print_options(
     form => $print_form,
     options => {dialog_name_prefix => 'print_options.',
                 show_headers       => 1,
@@ -1601,7 +1812,10 @@ sub pre_render {
                                                 } } @all_objects;
   }
 
-  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery edit_periodic_invoices_config calculate_qty);
+  $self->get_item_cvpartnumber($_) for @{$self->order->items_sorted};
+
+  $::request->{layout}->use_javascript("${_}.js") for qw(kivi.SalesPurchase kivi.Order kivi.File ckeditor/ckeditor ckeditor/adapters/jquery
+                                                         edit_periodic_invoices_config calculate_qty kivi.Validator);
   $self->setup_edit_action_bar;
 }
 
@@ -1620,7 +1834,7 @@ sub setup_edit_action_bar {
           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' ],
+          checks    => [ 'kivi.Order.check_save_active_periodic_invoices', ['kivi.validate_form','#order_form'] ],
         ],
         action => [
           t8('Save as new'),
@@ -1638,13 +1852,11 @@ sub setup_edit_action_bar {
           t8('Save and Sales Order'),
           submit   => [ '#order_form', { action => "Order/sales_order" } ],
           only_if  => (any { $self->type eq $_ } (sales_quotation_type(), purchase_order_type())),
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
         ],
         action => [
           t8('Save and Purchase Order'),
-          submit   => [ '#order_form', { action => "Order/purchase_order" } ],
-          only_if  => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
-          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
+          call      => [ 'kivi.Order.purchase_order_check_for_direct_delivery' ],
+          only_if   => (any { $self->type eq $_ } (sales_order_type(), request_quotation_type())),
         ],
         action => [
           t8('Save and Delivery Order'),
@@ -1659,6 +1871,12 @@ sub setup_edit_action_bar {
           call      => [ 'kivi.Order.save', 'save_and_invoice', $::instance_conf->get_order_warn_duplicate_parts ],
           checks    => [ 'kivi.Order.check_save_active_periodic_invoices' ],
         ],
+        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()))
+        ],
+
       ], # end of combobox "Workflow"
 
       combobox => [
@@ -1671,7 +1889,8 @@ sub setup_edit_action_bar {
         ],
         action => [
           t8('Save and E-mail'),
-          call => [ 'kivi.Order.email', $::instance_conf->get_order_warn_duplicate_parts ],
+          call => [ 'kivi.Order.save', 'save_and_show_email_dialog', $::instance_conf->get_order_warn_duplicate_parts ],
+          disabled => !$self->order->id ? t8('This object has not been saved yet.') : undef,
         ],
         action => [
           t8('Download attachments of all parts'),
@@ -1830,6 +2049,21 @@ sub get_title_for {
        : '';
 }
 
+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 sales_order_type {
   'sales_order';
 }
@@ -1869,9 +2103,8 @@ SL::Controller::Order - controller for orders
 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 expirience and a faster flow
-of work. Also the code should be more readable, more reliable and
-better to maintain.
+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
 
@@ -1902,7 +2135,7 @@ 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 does only reload on C<save>.
+with ajax-calls and the page only reloads on C<save>.
 
 =item *
 
@@ -1976,8 +2209,6 @@ java script functions
 
 =item * testing
 
-=item * currency
-
 =item * credit limit
 
 =item * more workflows (quotation, rfq)
@@ -1986,8 +2217,6 @@ java script functions
 
 =item * select units in input row?
 
-=item * custom shipto address
-
 =item * check for direct delivery (workflow sales order -> purchase order)
 
 =item * language / part translations
@@ -2042,10 +2271,6 @@ should be implemented.
 C<show_multi_items_dialog> does not use the currently inserted string for
 filtering.
 
-=item *
-
-The language selected in print or email dialog is not saved when the order is saved.
-
 =back
 
 =head1 To discuss / Nice to have
index 9595161..f589a71 100644 (file)
@@ -33,7 +33,7 @@ use Rose::Object::MakeMethods::Generic (
                                   all_buchungsgruppen all_payment_terms all_warehouses
                                   parts_classification_filter
                                   all_languages all_units all_price_factors) ],
-  'scalar'                => [ qw(warehouse bin) ],
+  'scalar'                => [ qw(warehouse bin stock_amounts journal) ],
 );
 
 # safety
@@ -263,6 +263,17 @@ sub action_history {
                                   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) = @_;
 
@@ -399,23 +410,28 @@ sub action_add_assembly_item {
 }
 
 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
+                all_partsgroups => SL::DB::Manager::PartsGroup->get_all,
+                search_term     => $search_term
   );
 }
 
 sub action_multi_items_update_result {
   my $max_count = 100;
 
-  $::form->{multi_items}->{filter}->{obsolete} = 0;
-
   my $count = $_[0]->multi_items_models->count;
 
   if ($count == 0) {
     my $text = escape($::locale->text('No results.'));
     $_[0]->render($text, { layout => 0 });
   } elsif ($count > $max_count) {
-    my $text = escpae($::locale->text('Too many results (#1 from #2).', $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;
@@ -564,7 +580,9 @@ sub action_ajax_autocomplete {
   # 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::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,
@@ -600,7 +618,13 @@ sub action_test_page {
 }
 
 sub action_part_picker_search {
-  $_[0]->render('part/part_picker_search', { layout => 0 });
+  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 {
@@ -890,6 +914,8 @@ sub init_part {
 
   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});
index 41ad793..85e3765 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 {
@@ -194,4 +200,3 @@ sub _make_record {
 }
 
 1;
-
index 14f27dc..76f85b2 100644 (file)
@@ -90,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 {
@@ -99,8 +99,8 @@ 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
+  # 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;
index 76127b1..9cf25a3 100644 (file)
@@ -179,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));
 
@@ -357,35 +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');
-    # veto either invoice_amount is fully assigned or not! No state tricks in later workflow!
-    # invoice_amount should be a distinct sign, that some bookings were really made from a bank transaction
-    # $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 {
index 875f170..6ba1c4e 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
@@ -217,9 +218,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};
 
@@ -599,11 +607,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;
index 2c045d2..ab31b8c 100644 (file)
@@ -17,6 +17,11 @@ sub action_check_duplicate_invnumber {
                    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' });
 }
index 2e559a3..aaca8e9 100644 (file)
@@ -35,6 +35,7 @@ my %supported_types = (
       { method => 'bank',                                      title => t8('Bank'), },
       { method => 'bank_code',                                 title => t8('Bank code'), },
       { method => 'bic',                                       title => t8('BIC'), },
+      {                                                        title => t8('Use for ZUGFeRD'), formatter => sub { $_[0]->use_for_zugferd ? 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' },
     ],
@@ -55,6 +56,26 @@ my %supported_types = (
     ],
   },
 
+  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 => {
@@ -64,6 +85,16 @@ my %supported_types = (
     },
   },
 
+  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',
index 5228038..43b4fee 100644 (file)
@@ -4,312 +4,513 @@ 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::ReportGenerator;
 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::Helper::AccountingPeriod qw(get_balance_starting_date);
-
-use SL::Presenter::Tag qw(checkbox_tag);
+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(charts charts9000 cbob_chart cb_date cb_startdate ob_date cb_reference ob_reference cb_description ob_description) ],
+  'scalar --get_set_init' => [ qw(cb_date cb_startdate ob_date) ],
 );
 
 __PACKAGE__->run_before('check_auth');
 
-sub action_filter {
+sub action_form {
   my ($self) = @_;
-  $self->ob_date(DateTime->today->truncate(to => 'year'))                  if !$self->ob_date;
-  $self->cb_date(DateTime->today->truncate(to => 'year')->add(days => -1)) if !$self->cb_date;
-  $self->ob_reference(t8('OB Transaction'))   if !$self->ob_reference;
-  $self->cb_reference(t8('CB Transaction'))   if !$self->cb_reference;
-  $self->ob_description(t8('OB Transaction')) if !$self->ob_description;
-  $self->cb_description(t8('CB Transaction')) if !$self->cb_description;
-
-  $self->setup_filter_action_bar;
-  $self->render('gl/yearend_filter',
-                title               => t8('CB/OB Transactions'),
-                make_title_of_chart => sub { $_[0]->accno.' '.$_[0]->description }
+
+  $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_list {
+sub action_get_start_date {
   my ($self) = @_;
-  $main::lxdebug->enter_sub();
 
-  my $report     = SL::ReportGenerator->new(\%::myconfig, $::form);
+  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->prepare_report($report);
+  $self->cb_startdate($::locale->parse_date_to_object($self->get_balance_starting_date($self->cb_date, $::form->{'balance_startdate_method'})));
 
-  $report->set_options(
-    output_format        => 'HTML',
-    raw_top_info_text    => $::form->parse_html_template('gl/yearend_top',    { SELF => $self }),
-    raw_bottom_info_text => $::form->parse_html_template('gl/yearend_bottom', { SELF => $self }),
-    allow_pdf_export     => 0,
-    allow_csv_export     => 0,
-    title                => $::locale->text('CB/OB Transactions'),
-  );
+  # $main::lxdebug->message(0, "found start date: ", $self->cb_startdate->to_kivitendo);
 
-  $self->setup_list_action_bar;
-  $report->generate_with_headers();
-  $main::lxdebug->leave_sub();
+  return $self->js->val('#cb_startdate', $self->cb_startdate->to_kivitendo)
+              ->show('#apply_year_end_bookings_button')
+              ->show('.startdate')
+              ->render;
 }
 
-sub action_generate {
+sub action_update_charts {
   my ($self) = @_;
 
-  if ($self->cb_date > $self->ob_date) {
-    flash ('error', $::locale->text('CB date #1 is higher than OB date #2. Please select again.', $self->cb_date, $self->ob_date));
-  } else {
-    my $cnt = $self->make_booking();
-    flash('info', $::locale->text('#1 CB transactions and #1 OB transactions generated.',$cnt)) if $cnt > 0;
-  }
-  $self->action_list;
-}
+  $self->_parse_form;
 
-sub check_auth {
-  $::auth->assert('general_ledger');
+  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 make_booking {
+sub _parse_form {
   my ($self) = @_;
-  $main::lxdebug->enter_sub();
-  my @ids = map { $::form->{"multi_id_$_"} } grep { $::form->{"multi_id_$_"} } (1..$::form->{rowcount});
-  my $cnt = 0;
-  $main::lxdebug->message(LXDebug->DEBUG2(),"generate for ".$::form->{cbob_chart}." # ".scalar(@ids)." charts");
-  if (scalar(@ids) && $::form->{cbob_chart}) {
-    my $carryoverchart = SL::DB::Manager::Chart->get_first(  query => [ id => $::form->{cbob_chart} ] );
-    my $charts = SL::DB::Manager::Chart->get_all(  query => [ id => \@ids ] );
-    foreach my $chart (@{ $charts }) {
-      $main::lxdebug->message(LXDebug->DEBUG2(),"chart_id=".$chart->id." accno=".$chart->accno);
-      my $balance = $self->get_balance($chart);
-      if ( $balance != 0 ) {
-        # SB
-        $self->gl_booking($balance,$self->cb_date,$::form->{cb_reference},$::form->{cb_description},$chart,$carryoverchart,0,1);
-        # EB
-        $self->gl_booking($balance,$self->ob_date,$::form->{ob_reference},$::form->{ob_description},$carryoverchart,$chart,1,0);
-        $cnt++;
-      }
-    }
-  }
-  $main::lxdebug->leave_sub();
-  return $cnt;
+
+  # 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) = @_;
 
-sub prepare_report {
-  my ($self,$report) = @_;
-  $main::lxdebug->enter_sub();
-  my $idx = 1;
+  my $start_date = delete $params{start_date};
+  my $cb_date    = delete $params{cb_date};
 
-  my %column_defs = (
-    'ids'         => { raw_header_data => checkbox_tag("", id => "check_all",
-                                                                          checkall => "[data-checkall=1]"), 'align' => 'center' },
-    'chart'       => { text => $::locale->text('Account'), },
-    'description' => { text => $::locale->text('Description'), },
-    'saldo'       => { text => $::locale->text('Saldo'),  'align' => 'right'},
-    'sum_cb'      => { text => $::locale->text('Sum CB Transactions'), 'align' => 'right'},  ##close == Schluss
-    'sum_ob'      => { text => $::locale->text('Sum OB Transactions'), 'align' => 'right'},  ##open  == Eingang
-  );
-  my @columns      = qw(ids chart description saldo sum_cb sum_ob);
-  map { $column_defs{$_}->{visible} = 1 } @columns;
+  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 $ob_next_date = $self->ob_date->clone();
-  $ob_next_date->add(years => 1)->add(days => -1);
+  my ($report_data, $profit_loss_sum) = _report(
+                                                start_date => $start_date,
+                                                cb_date    => $cb_date,
+                                               );
 
-  $self->cb_startdate($::locale->parse_date_to_object($self->get_balance_starting_date($self->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 @custom_headers = ();
-  # Zeile 1:
-  push @custom_headers, [
-      { 'text' => '   ', 'colspan' => 3 },
-      { 'text' => $::locale->text("Timerange")."<br />".$self->cb_startdate->to_kivitendo." - ".$self->cb_date->to_kivitendo, 'colspan' => 2, 'align' => 'center'},
-      { 'text' => $::locale->text("Timerange")."<br />".$self->ob_date->to_kivitendo." - ".$ob_next_date->to_kivitendo, 'align' => 'center'},
-    ];
-
-  # Zeile 2:
-  my @line_2 = ();
-  map { push @line_2 , $column_defs{$_} } grep { $column_defs{$_}->{visible} } @columns;
-  push @custom_headers, [ @line_2 ];
-
-  $report->set_custom_headers(@custom_headers);
-  $report->set_columns(%column_defs);
-  $report->set_column_order(@columns);
-
-  my $chart9actual = SL::DB::Manager::Chart->get_first( query => [ id => $self->cbob_chart ] );
-  $self->{cbob_chartaccno} = $chart9actual->accno.' '.$chart9actual->description;
-
-  foreach my $chart (@{ $self->charts }) {
-    my $balance = $self->get_balance($chart);
-    if ( $balance != 0 ) {
-      my $chart_id = $chart->id;
-      my $row = { map { $_ => { 'data' => '' } } @columns };
-      $row->{ids}  = {
-        'raw_data' => checkbox_tag("multi_id_${idx}", value => $chart_id, "data-checkall" => 1),
-        'valign'   => 'center',
-        'align'    => 'center',
-      };
-      $row->{chart}->{data}       = $chart->accno;
-      $row->{description}->{data} = $chart->description;
-      if ( $balance > 0 ) {
-        $row->{saldo}->{data} = $::form->format_amount(\%::myconfig, $balance, 2)." H";
-      } elsif ( $balance < 0 )  {
-        $row->{saldo}->{data} = $::form->format_amount(\%::myconfig,-$balance, 2)." S";
-      } else {
-        $row->{saldo}->{data} = $::form->format_amount(\%::myconfig,0, 2)."  ";
-      }
-      my $sum_cb = 0;
-      foreach my $acc ( @{ SL::DB::Manager::AccTransaction->get_all(where => [ chart_id  => $chart->id, cb_transaction => 't',
-                                                                               transdate => { ge => $self->cb_startdate},
-                                                                               transdate => { le => $self->cb_date }
-                                                                             ]) }) {
-        $sum_cb += $acc->amount;
-      }
-      my $sum_ob = 0;
-      foreach my $acc ( @{ SL::DB::Manager::AccTransaction->get_all(where => [ chart_id  => $chart->id, ob_transaction => 't',
-                                                                               transdate => { ge => $self->ob_date},
-                                                                               transdate => { le => $ob_next_date }
-                                                                             ]) }) {
-        $sum_ob += $acc->amount;
-      }
-      if ( $sum_cb > 0 ) {
-        $row->{sum_cb}->{data} = $::form->format_amount(\%::myconfig, $sum_cb, 2)." H";
-      } elsif ( $sum_cb < 0 )  {
-        $row->{sum_cb}->{data} = $::form->format_amount(\%::myconfig,-$sum_cb, 2)." S";
-      } else {
-        $row->{sum_cb}->{data} = $::form->format_amount(\%::myconfig,0, 2)."  ";
-      }
-      if ( $sum_ob > 0 ) {
-        $row->{sum_ob}->{data} = $::form->format_amount(\%::myconfig, $sum_ob, 2)." H";
-      } elsif ( $sum_ob < 0 )  {
-        $row->{sum_ob}->{data} = $::form->format_amount(\%::myconfig,-$sum_ob, 2)." S";
-      } else {
-        $row->{sum_ob}->{data} = $::form->format_amount(\%::myconfig,0, 2)."  ";
-      }
-      $report->add_data($row);
-    }
-    $idx++;
-  }
+  my @asset_accounts       = grep { $_->{account_type} eq 'asset_account' }       @{ $report_data };
+  my @profit_loss_accounts = grep { $_->{account_type} eq 'profit_loss_account' } @{ $report_data };
 
-  $self->{row_count} = $idx;
-  $main::lxdebug->leave_sub();
-}
+  my $ob_date = $cb_date->clone->add(days => 1);
 
-sub get_balance {
-  $main::lxdebug->enter_sub();
-  my ($self,$chart) = @_;
+  my ($credit_sum, $debit_sum) = (0,0);
 
-  #$main::lxdebug->message(LXDebug->DEBUG2(),"get_balance from=".$self->cb_startdate->to_kivitendo." to=".$self->cb_date->to_kivitendo);
-  my $balance = $chart->get_balance(fromdate => $self->cb_startdate, todate => $self->cb_date);
-  $main::lxdebug->leave_sub();
-  return 0 if !defined $balance || $balance == 0;
-  return $balance;
-}
+  my $employee_id = SL::DB::Manager::Employee->current->id;
 
-sub gl_booking {
-  my ($self, $amount, $transdate, $reference, $description, $konto, $gegenkonto, $ob, $cb) = @_;
-  $::form->get_employee();
-  my $employee_id = $::form->{employee_id};
-  $main::lxdebug->message(LXDebug->DEBUG2(),"employee_id=".$employee_id." ob=".$ob." cb=".$cb);
-  my $gl_entry = SL::DB::GLTransaction->new(
-    employee_id    => $employee_id,
-    transdate      => $transdate,
-    reference      => $reference,
-    description    => $description,
-    ob_transaction => $ob,
-    cb_transaction => $cb,
-  );
-  #$gl_entry->save;
-  my $kto_trans1 = SL::DB::AccTransaction->new(
-    trans_id       => $gl_entry->id,
-    transdate      => $transdate,
-    ob_transaction => $ob,
-    cb_transaction => $cb,
-    chart_id       => $gegenkonto->id,
-    chart_link     => $konto->link,
-    tax_id         => 0,
-    taxkey         => 0,
-    amount         => $amount,
-  );
-  #$kto_trans1->save;
-  my $kto_trans2 = SL::DB::AccTransaction->new(
-    trans_id       => $gl_entry->id,
-    transdate      => $transdate,
-    ob_transaction => $ob,
-    cb_transaction => $cb,
-    chart_id       => $konto->id,
-    chart_link     => $konto->link,
-    tax_id         => 0,
-    taxkey         => 0,
-    amount         => -$amount,
-  );
-  #$kto_trans2->save;
-  $gl_entry->add_transactions($kto_trans1);
-  $gl_entry->add_transactions($kto_trans2);
-  $gl_entry->save;
-}
+  # 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:
 
-sub init_cbob_chart     { $::form->{cbob_chart}                                    }
-sub init_ob_date        { $::locale->parse_date_to_object($::form->{ob_date})      }
-sub init_ob_reference   { $::form->{ob_reference}                                  }
-sub init_ob_description { $::form->{ob_description}                                }
-sub init_cb_startdate   { $::locale->parse_date_to_object($::form->{cb_startdate}) }
-sub init_cb_date        { $::locale->parse_date_to_object($::form->{cb_date})      }
-sub init_cb_reference   { $::form->{cb_reference}                                  }
-sub init_cb_description { $::form->{cb_description}                                }
+  # * cb for credit
+  # * cb for debit
+  # * ob for credit
+  # * ob for debit
 
-sub init_charts9000 {
-  SL::DB::Manager::Chart->get_all(  query => [ accno => { like => '9%'}] );
-}
+  my $db = SL::DB->client;
+  $db->with_transaction(sub {
 
-sub init_charts {
-  # wie geht 'not like' in rose ?
-  SL::DB::Manager::Chart->get_all(  query => [ \ "accno not like '9%'"], sort_by => 'accno ASC' );
-}
+    ######### asset accounts ########
+    # need cb and ob transactions
 
-sub setup_filter_action_bar {
-  my ($self) = @_;
+    my $debit_balance  = 0;
+    my $credit_balance = 0;
 
-  for my $bar ($::request->layout->get('actionbar')) {
-    $bar->add(
-      action => [
-        t8('Continue'),
-        submit    => [ '#filter_form', { action => 'YearEndTransactions/list' } ],
-        accesskey => 'enter',
-      ],
+    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   => [],
     );
-  }
-}
 
-sub setup_list_action_bar {
-  my ($self) = @_;
+    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
+        );
 
-  for my $bar ($::request->layout->get('actionbar')) {
-    $bar->add(
-      action => [
-        t8('Post'),
-        submit    => [ '#form', { action => 'YearEndTransactions/generate' } ],
-        tooltip   => t8('generate cb/ob transactions for selected charts'),
-        confirm   => t8('Are you sure to generate cb/ob transactions?'),
-        accesskey => 'enter',
-      ],
-      action => [
-        t8('Back'),
-        call => [ 'kivi.history_back' ],
-      ],
+      } 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..29d7e07
--- /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('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 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 ZUGFeRD invoice," .
+         " please ask your vendor to add this for his 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: " . $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 51fe4bd..af188f7 100644 (file)
@@ -37,6 +37,7 @@ use SL::DB;
 use SL::HTML::Util ();
 use SL::Iconv;
 use SL::Locale::String qw(t8);
+use SL::VATIDNr;
 
 use Data::Dumper;
 use DateTime;
@@ -574,7 +575,7 @@ sub generate_datev_data {
        UNION ALL
 
        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, NULL as deliverydate, gl.itime::date,
+         gl.reference AS invnumber, NULL AS duedate, ac.amount as umsatz, gl.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,
@@ -1045,11 +1046,26 @@ sub generate_datev_lines {
         $datev_data{buchungstext} = $transaction->[$haben]->{'name'};
       }
       if (($transaction->[$haben]->{'ustid'} // '') ne "") {
-        $datev_data{ustid} = $transaction->[$haben]->{'ustid'};
+        $datev_data{ustid} = SL::VATIDNr->normalize($transaction->[$haben]->{'ustid'});
       }
       if (($transaction->[$haben]->{'duedate'} // '') ne "") {
         $datev_data{belegfeld2} = $transaction->[$haben]->{'duedate'};
       }
+
+      # 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???
 
@@ -1070,6 +1086,8 @@ sub generate_datev_lines {
       # $datev_data{buchungsschluessel} = !$datevautomatik ? $taxkey : "4";
       $datev_data{buchungsschluessel} = $taxkey;
     }
+    # set lock for each transaction
+    $datev_data{locked} = $self->locked;
 
     push(@datev_lines, \%datev_data) if $datev_data{umsatz};
   }
@@ -1162,7 +1180,7 @@ sub kne_buchungsexport {
     $name =~ s/\ *$//;
     $kne_file->add_block("\x1E" . $name . "\x1C");
 
-    $kne_file->add_block("\xBA" . $kne->{'ustid'}    . "\x1C") if $kne->{'ustid'};
+    $kne_file->add_block("\xBA" . SL::VATIDNr->normalize($kne->{'ustid'}) . "\x1C") if $kne->{'ustid'};
 
     $kne_file->add_block("\xB3" . $kne->{'waehrung'} . "\x1C" . "\x79");
   };
@@ -1631,7 +1649,7 @@ 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
 
index e24755b..8b8e885 100644 (file)
@@ -11,6 +11,7 @@ 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) ],
@@ -134,6 +135,7 @@ my @kivitendo_to_datev = (
                                                        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',
@@ -239,16 +241,267 @@ my @kivitendo_to_datev = (
                               input_check     => sub {
                                                        my ($ustid) = @_;
                                                        return 1 if ('' eq $ustid);
-                                                       $ustid =~ s{\s+}{}g;
-                                                       return ($ustid =~ m/^CH|^[A-Z]{2}\w{5,13}$/);
+                                                       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 ($ustid =~ m/^CH|^[A-Z]{2}\w{5,13}$/);
+                                                       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 {
@@ -275,10 +528,6 @@ sub check_encoding {
   }
 }
 
-sub _kivitendo_to_datev {
-  @kivitendo_to_datev, ({ kivi_datev_name => 'not yet implemented' }) x (116 - @kivitendo_to_datev);
-}
-
 sub header {
   my ($self) = @_;
 
@@ -316,7 +565,7 @@ sub header {
   push @header, [ @header_row_1 ];
 
   # second header row, just the column names
-  push @header, [ map { $_->{csv_header_name} } _kivitendo_to_datev() ];
+  push @header, [ map { $_->{csv_header_name} } @kivitendo_to_datev ];
 
   return \@header;
 }
@@ -325,7 +574,6 @@ sub lines {
   my ($self) = @_;
 
   my (@array_of_datev, @warnings);
-  my @csv_columns = _kivitendo_to_datev();
 
   foreach my $row (@{ $self->datev_lines }) {
     my @current_datev_row;
@@ -333,7 +581,7 @@ sub lines {
     # 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 (@csv_columns) {
+    foreach my $column (@kivitendo_to_datev) {
       if ($column->{kivi_datev_name} eq 'not yet implemented') {
         push @current_datev_row, '';
         next;
index 60681c7..a59fd6f 100644 (file)
--- a/SL/DB.pm
+++ b/SL/DB.pm
@@ -205,7 +205,7 @@ within another C<with_transaction>, whereas L<Rose::DB/do_transaction> can not.
 =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 occured. This way you can
+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>:
 
@@ -222,19 +222,19 @@ or you can use it to safely calulate things.
 
 =item Error handling
 
-The original L<Rose::DB/do_transaction> gobbles up all execptions and expects
-the caller to manually check 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.
+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 exception that are used to signal an
+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 typoes,
+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 rollbacked in both cases.
+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:
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 b9c34eb..5249410 100644 (file)
@@ -65,8 +65,17 @@ sub destroy {
     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);
+      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 };
index cb51f90..71c762a 100644 (file)
@@ -10,6 +10,7 @@ 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,
@@ -61,6 +62,7 @@ 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;
 }
@@ -98,4 +100,12 @@ 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;
+}
+
 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 a92271f..25d4de7 100644 (file)
@@ -5,6 +5,9 @@ use strict;
 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;
@@ -57,4 +60,282 @@ sub invnumber {
 
 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 $tax = SL::DB::Manager::Tax->find_by(id => $params{tax_id})
+    // croak "Can't find tax with id " . $params{tax_id};
+
+  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!') 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, ...
+
+Mandatory params are
+
+=over 2
+
+=item * chart as an RDBO object
+
+=item * tax_id
+
+=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 e6a6982..006a389 100644 (file)
@@ -27,6 +27,8 @@ 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;
@@ -62,6 +64,7 @@ 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;
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 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 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] }
index 1228acc..7c001af 100644 (file)
@@ -37,21 +37,29 @@ sub flatten_to_form {
 
   my @vc_fields          = (qw(account_number bank bank_code bic business city contact country creditlimit
                                department_1 department_2 discount email fax gln greeting homepage iban language name
-                               phone street taxnumber ustid zipcode),
+                               natural_person phone street taxnumber ustid zipcode),
                             "${vc}number",
                             ($vc eq 'customer')? 'c_vendor_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
@@ -132,7 +140,7 @@ sub _copy {
 sub _copy_custom_variables {
   my ($src, $form, $prefix, $postfix, $cvar_validity) = @_;
 
-  my $obj = (any { ref($src) eq $_ } qw(SL::DB::OrderItem SL::DB::DeliveryOrderItem SL::DB::InvoiceItem SL::DB::Contact))
+  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;
 
index 4d1c5af..da931f9 100644 (file)
@@ -110,6 +110,8 @@ my %kivitendo_package_names = (
   bin                            => 'bin',
   business                       => 'business',
   chart                          => 'chart',
+  contact_departments            => 'contact_department',
+  contact_titles                 => 'contact_title',
   contacts                       => 'contact',
   customer                       => 'customer',
   csv_import_profiles            => 'csv_import_profile',
@@ -146,6 +148,7 @@ my %kivitendo_package_names = (
   follow_ups                     => 'follow_up',
   generic_translations           => 'generic_translation',
   gl                             => 'GLTransaction',
+  greetings                      => 'greeting',
   history_erp                    => 'history',
   inventory                      => 'inventory',
   invoice                        => 'invoice_item',
diff --git a/SL/DB/Helper/PDF_A.pm b/SL/DB/Helper/PDF_A.pm
new file mode 100644 (file)
index 0000000..1e1e172
--- /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 => 'ZUGFeRD-invoice.xml',
+        document_type      => 'INVOICE',
+        version            => '1.0',
+      },
+    ),
+  };
+}
+
+1;
index 55f9029..a778e92 100644 (file)
@@ -58,10 +58,10 @@ sub pay_invoice {
   }
 
   my $transdate_obj;
-  if (ref($params{transdate} eq 'DateTime')) {
+  if (ref($params{transdate}) eq 'DateTime') {
     $transdate_obj = $params{transdate};
   } else {
-   $transdate_obj = $::locale->parse_date_to_object($params{transdate});
+    $transdate_obj = $::locale->parse_date_to_object($params{transdate});
   };
   croak t8('Illegal date') unless ref $transdate_obj;
 
@@ -338,21 +338,9 @@ 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 {
@@ -442,19 +430,11 @@ 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;
 };
@@ -483,6 +463,12 @@ sub check_skonto_configuration {
   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, 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) };
@@ -534,8 +520,7 @@ sub skonto_charts {
   # 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
+  # return undef unless $self->percent_skonto;
 
   my $is_sales = ref($self) eq 'SL::DB::Invoice';
 
@@ -886,6 +871,7 @@ 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 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.
@@ -898,12 +884,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.
index a3c42d2..f5bc61c 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           => { },
@@ -53,7 +54,7 @@ sub calculate_prices_and_taxes {
   $self->netamount(  0);
   $self->marge_total(0);
 
-  SL::DB::Manager::Chart->cache_taxkeys(date => $self->transdate);
+  SL::DB::Manager::Chart->cache_taxkeys(date => $self->deliverydate // $self->transdate);
 
   my $idx = 0;
   foreach my $item (@{ $self->items_sorted }) {
@@ -65,7 +66,7 @@ sub calculate_prices_and_taxes {
 
   return $self unless wantarray;
 
-  return map { ($_ => $data{$_}) } qw(taxes amounts amounts_cogs allocated exchangerate assembly_items items rounding);
+  return map { ($_ => $data{$_}) } qw(taxes_by_chart_id taxes_by_tax_id amounts amounts_cogs allocated exchangerate assembly_items items rounding);
 }
 
 sub _get_exchangerate {
@@ -109,7 +110,7 @@ sub _calculate_item {
 
   $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->deliverydate // $self->transdate, is_sales => $data->{is_sales}, taxzone => $self->taxzone_id);
   my $tax_rate   = $taxkey->tax->rate;
   my $tax_amount = undef;
 
@@ -122,8 +123,10 @@ sub _calculate_item {
   }
 
   if ($taxkey->tax->chart_id) {
-    $data->{taxes}->{ $taxkey->tax->chart_id } ||= 0;
-    $data->{taxes}->{ $taxkey->tax->chart_id }  += $tax_amount;
+    $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;
   }
@@ -180,10 +183,10 @@ 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} });
@@ -199,7 +202,7 @@ sub _calculate_amounts {
 
   _dbg("Sna " . $self->netamount . " idiff " . $data->{invoicediff} . " tdiff ${tax_diff}");
 
-  my $tax              = sum values %{ $data->{taxes} };
+  my $tax              = sum values %{ $data->{taxes_by_chart_id} };
   $amount              = $netamount + $tax;
   my $grossamount      = _round($amount, 2, 1);
   $data->{rounding}    = _round($grossamount - $amount, 2);
@@ -335,9 +338,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/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/ZUGFeRD.pm b/SL/DB/Helper/ZUGFeRD.pm
new file mode 100644 (file)
index 0000000..211e885
--- /dev/null
@@ -0,0 +1,664 @@
+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 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);
+
+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} };
+
+  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 ($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->customer->create_zugferd_invoices_for_this_customer == 2) {
+    $params{xml}->startTag("ram:TestIndicator");
+    $params{xml}->dataElement("udt:Indicator", "true");
+    $params{xml}->endTag;
+  }
+
+  $params{xml}->startTag("ram:GuidelineSpecifiedDocumentContextParameter");
+  $params{xml}->dataElement("ram:ID", "urn:cen.eu:en16931:2017#conformant#urn:zugferd.de:2p0:extended");
+  $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)));
+  $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 ($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_cfg{name} || $sales_person_cfg{login}));
+
+  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);
+
+  $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");
+
+  _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 ZUGFeRD usage was found.'));
+    }
+  }
+
+  return %result;
+}
+
+sub create_zugferd_data {
+  my ($self)        = @_;
+
+  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 => 'ZUGFeRD-invoice.xml',
+    document_type      => 'INVOICE',
+    version            => '1.0',
+  };
+}
+
+1;
index d6ec6c8..93c17e4 100644 (file)
@@ -13,9 +13,11 @@ 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::TransNumberGenerator;
+use SL::DB::Helper::ZUGFeRD;
 use SL::Locale::String qw(t8);
 use SL::DB::CustomVariable;
 
@@ -172,7 +174,7 @@ sub new_from {
 
   my %args = ( map({ ( $_ => $source->$_ ) } qw(customer_id taxincluded shippingpoint shipvia notes intnotes salesman_id cusordnumber ordnumber department_id
                                                 cp_id language_id taxzone_id globalproject_id transaction_description currency_id delivery_term_id), @columns),
-               transdate   => DateTime->today_local,
+               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,
@@ -184,9 +186,16 @@ sub new_from {
 
   $args{payment_id} = ( $terms ? $terms->id : $source->payment_id);
 
-  if ($source->type =~ /_order$/) {
+  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;
   }
@@ -266,7 +275,7 @@ 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 });
 
@@ -296,14 +305,16 @@ sub _post_add_acctrans {
     $chart_link = SL::DB::Manager::Chart->find_by(id => $chart_id)->{'link'};
     $chart_link ||= '';
 
-    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;
+    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;
+    }
   }
 }
 
@@ -355,6 +366,7 @@ sub add_ar_amount_row {
     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},
@@ -369,6 +381,7 @@ sub add_ar_amount_row {
        chart_id   => $tax->chart_id,
        chart_link => $tax->chart->link,
        transdate  => $self->transdate,
+       gldate     => $self->gldate,
        taxkey     => $tax->taxkey,
        tax_id     => $tax->id,
      );
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;
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;
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;
index d24954b..b46f851 100644 (file)
@@ -23,7 +23,17 @@ __PACKAGE__->add_filter_specs(
   all => sub {
     my ($key, $value, $prefix) = @_;
     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 {
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 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;
index 47eb3cf..4e8fe0e 100644 (file)
@@ -16,7 +16,7 @@ __PACKAGE__->meta->columns(
   mtime       => { type => 'timestamp' },
   parts_id    => { type => 'integer', not_null => 1 },
   position    => { type => 'integer' },
-  qty         => { type => 'float', scale => 4 },
+  qty         => { type => 'float', precision => 4, scale => 4 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'assembly_id' ]);
index 9f21743..8bd1611 100644 (file)
@@ -15,7 +15,7 @@ __PACKAGE__->meta->columns(
   mtime         => { type => 'timestamp' },
   parts_id      => { type => 'integer', not_null => 1 },
   position      => { type => 'integer', not_null => 1 },
-  qty           => { type => 'float', not_null => 1, scale => 4 },
+  qty           => { type => 'float', not_null => 1, precision => 4, scale => 4 },
   unit          => { type => 'varchar', length => 20, not_null => 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..5589ee4 100644 (file)
@@ -21,6 +21,7 @@ __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_zugferd                 => { type => 'boolean', default => 'false', not_null => 1 },
 );
 
 __PACKAGE__->meta->primary_key_columns([ 'id' ]);
index 6428c99..b4ba2e2 100644 (file)
@@ -14,12 +14,11 @@ __PACKAGE__->meta->columns(
   ar_id               => { type => 'integer' },
   bank_transaction_id => { type => 'integer', not_null => 1 },
   gl_id               => { type => 'integer' },
-  id                  => { type => 'serial', not_null => 1 },
   itime               => { type => 'timestamp', default => 'now()' },
   mtime               => { type => 'timestamp' },
 );
 
-__PACKAGE__->meta->primary_key_columns([ 'id' ]);
+__PACKAGE__->meta->primary_key_columns([ 'bank_transaction_id', 'acc_trans_id' ]);
 
 __PACKAGE__->meta->allow_inline_column_values(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 36afa03..419375f 100644 (file)
@@ -18,7 +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' },
+  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 775ee8b..d0e1b14 100644 (file)
@@ -22,6 +22,7 @@ __PACKAGE__->meta->columns(
   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' },
@@ -48,6 +49,7 @@ __PACKAGE__->meta->columns(
   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' },
index c0f6a44..7a4b5de 100644 (file)
@@ -10,7 +10,11 @@ __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' },
   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 },
@@ -30,12 +34,16 @@ __PACKAGE__->meta->columns(
   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_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 },
@@ -100,6 +108,7 @@ __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' },
@@ -114,6 +123,7 @@ __PACKAGE__->meta->columns(
   pdonumber                                 => { type => 'text' },
   ponumber                                  => { type => 'text' },
   precision                                 => { type => 'numeric', default => '0.01', not_null => 1, precision => 15, scale => 5 },
+  profit_carried_forward_chart_id           => { type => 'integer' },
   profit_determination                      => { type => 'text' },
   project_status_id                         => { type => 'integer' },
   project_type_id                           => { type => 'integer' },
@@ -131,6 +141,7 @@ __PACKAGE__->meta->columns(
   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_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' },
@@ -159,6 +170,7 @@ __PACKAGE__->meta->columns(
   transfer_default_use_master_default_bin   => { type => 'boolean', default => 'false' },
   transfer_default_warehouse_for_assembly   => { type => 'boolean', default => 'false' },
   transport_cost_reminder_article_number_id => { type => 'integer' },
+  vc_greetings_use_textfield                => { type => 'boolean' },
   vendornumber                              => { type => 'text' },
   version                                   => { type => 'varchar', length => 8 },
   vertreter                                 => { type => 'boolean', default => 'false' },
@@ -167,6 +179,7 @@ __PACKAGE__->meta->columns(
   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' ]);
@@ -194,11 +207,26 @@ __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' },
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 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 3b829d5..f592513 100644 (file)
@@ -10,6 +10,7 @@ __PACKAGE__->meta->table('gl');
 
 __PACKAGE__->meta->columns(
   cb_transaction => { type => 'boolean' },
+  deliverydate   => { type => 'date' },
   department_id  => { type => 'integer' },
   description    => { type => 'text' },
   employee_id    => { type => 'integer' },
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 2da0d7f..85b8876 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 => 'float', precision => 4, scale => 4 },
   sellprice              => { type => 'numeric', precision => 15, scale => 5 },
   serialnumber           => { type => 'text' },
   subtotal               => { type => 'boolean', default => 'false' },
index b359c07..d6922bd 100644 (file)
@@ -21,6 +21,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' },
index 9de8b7e..16478b1 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 },
@@ -30,11 +30,11 @@ __PACKAGE__->meta->columns(
   price_factor_id        => { type => 'integer' },
   pricegroup_id          => { type => 'integer' },
   project_id             => { type => 'integer' },
-  qty                    => { type => 'float', scale => 4 },
+  qty                    => { type => 'float', precision => 4, scale => 4 },
   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 6ba8c00..876474e 100644 (file)
@@ -17,7 +17,7 @@ __PACKAGE__->meta->columns(
   itime                    => { type => 'timestamp', default => 'now()' },
   mtime                    => { type => 'timestamp' },
   obsolete                 => { type => 'boolean', default => 'false' },
-  percent_skonto           => { type => 'float', scale => 4 },
+  percent_skonto           => { type => 'float', precision => 4, scale => 4 },
   sortkey                  => { type => 'integer', not_null => 1 },
   terms_netto              => { type => 'integer' },
   terms_skonto             => { type => 'integer' },
index f4a2e2d..55aba86 100644 (file)
@@ -19,7 +19,6 @@ __PACKAGE__->meta->columns(
   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' ]);
index e3a1b93..a457802 100644 (file)
@@ -26,7 +26,7 @@ __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' },
@@ -39,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 f2f12c0..3e0fca6 100755 (executable)
@@ -26,6 +26,7 @@ my %db_to_presenter_mapping = (
   Customer        => 'CustomerVendor',
   PurchaseInvoice => 'Invoice',
   Vendor          => 'CustomerVendor',
+  GLTransaction   => 'GL',
 );
 
 sub new {
index bba7fc8..d996d13 100644 (file)
@@ -10,6 +10,7 @@ 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;
@@ -17,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);
 
@@ -40,14 +42,24 @@ __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' },
+  },
 );
 
+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
 
@@ -63,6 +75,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
 
@@ -85,6 +138,12 @@ sub is_type {
   return shift->type eq shift;
 }
 
+sub deliverydate {
+  # oe doesn't have deliverydate, but PTC checks for deliverydate or transdate to determine tax
+  # oe can't deal with deviating tax rates, but at least make sure PTC doesn't barf
+  return shift->transdate;
+}
+
 sub displayable_type {
   my $type = shift->type;
 
@@ -105,6 +164,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 = @_;
@@ -235,7 +321,7 @@ sub new_from {
   }
 
   my %args = ( map({ ( $_ => $source->$_ ) } qw(amount cp_id currency_id cusordnumber customer_id delivery_customer_id delivery_term_id delivery_vendor_id
-                                                department_id employee_id globalproject_id intnotes marge_percent marge_total language_id netamount notes
+                                                department_id employee_id exchangerate globalproject_id intnotes marge_percent marge_total language_id netamount notes
                                                 ordnumber payment_id quonumber reqdate salesman_id shippingpoint shipvia taxincluded taxzone_id
                                                 transaction_description vendor_id
                                              )),
@@ -247,6 +333,7 @@ sub new_from {
 
   if ( $is_abbr_any->(qw(sopo poso)) ) {
     $args{ordnumber} = undef;
+    $args{quonumber} = undef;
     $args{reqdate}   = DateTime->today_local->next_workday();
     $args{employee}  = SL::DB::Manager::Employee->current;
   }
@@ -350,7 +437,7 @@ sub new_from_multi {
   }
   foreach my $attr (qw(cp_id currency_id employee_id salesman_id department_id
                        delivery_customer_id delivery_vendor_id shipto_id
-                       globalproject_id)) {
+                       globalproject_id exchangerate)) {
     $attributes{$attr} = undef if any { ($sources->[0]->$attr||0) != ($_->$attr||0) }   @$sources;
   }
 
@@ -453,6 +540,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
index a92e2f3..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;
@@ -93,5 +90,3 @@ Alias for L</shipped_qty>.
 G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
 
 =cut
-
-
index 2937136..e98b0c4 100644 (file)
@@ -3,7 +3,7 @@ package SL::DB::Part;
 use strict;
 
 use Carp;
-use List::MoreUtils qw(any);
+use List::MoreUtils qw(any uniq);
 use Rose::DB::Object::Helpers qw(as_tree);
 
 use SL::Locale::String qw(t8);
@@ -32,7 +32,7 @@ __PACKAGE__->meta->add_relationships(
   assemblies                     => {
     type         => 'one to many',
     class        => 'SL::DB::Assembly',
-    manager_args => { sort_by => 'position, oid' },
+    manager_args => { sort_by => 'position' },
     column_map   => { id => 'id' },
   },
   prices         => {
@@ -61,6 +61,7 @@ __PACKAGE__->meta->add_relationships(
     type         => 'one to many',
     class        => 'SL::DB::AssortmentItem',
     column_map   => { id => 'assortment_id' },
+    manager_args => { sort_by => 'position' },
   },
   history_entries   => {
     type            => 'one to many',
@@ -80,7 +81,8 @@ __PACKAGE__->meta->add_relationships(
 __PACKAGE__->meta->initialize;
 
 __PACKAGE__->attr_html('notes');
-__PACKAGE__->attr_sorted({ unsorted => 'makemodels', position => 'sortorder' });
+__PACKAGE__->attr_sorted({ unsorted => 'makemodels',     position => 'sortorder' });
+__PACKAGE__->attr_sorted({ unsorted => 'customerprices', position => 'sortorder' });
 
 __PACKAGE__->before_save('_before_save_set_partnumber');
 
@@ -352,6 +354,92 @@ sub get_simple_stock {
   sub bin       { require SL::DB::Bin;       SL::DB::Manager::Bin      ->find_by_or_create(id => $_[0]->{bin_id}) }
 }
 
+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" ] ],
+    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) = @_;
 
@@ -567,6 +655,23 @@ 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.
index ceb532a..0523348 100644 (file)
@@ -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;
index 93fc1fe..a50884c 100644 (file)
@@ -182,6 +182,7 @@ sub add_ap_amount_row {
     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},
@@ -196,6 +197,7 @@ sub add_ap_amount_row {
        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},
index 99a4036..6cfcba6 100644 (file)
@@ -3,6 +3,9 @@ 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;
@@ -16,6 +19,7 @@ our @SHIPTO_VARIABLES = qw(shiptoname shiptostreet shiptozipcode shiptocity ship
 
 __PACKAGE__->meta->initialize;
 
+
 sub displayable_id {
   my $self = shift;
   my $text = join('; ', grep { $_ } (map({ $self->$_ } qw(shiptoname shiptostreet)),
@@ -40,6 +44,15 @@ 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];
@@ -90,6 +103,18 @@ SL::DB::Shipto - Database model for shipping addresses
 
 =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
index ee02f3e..4e125fb 100644 (file)
@@ -26,8 +26,9 @@ __PACKAGE__->meta->initialize;
 sub convert_to_sales_order {
   my ($self, %params) = @_;
 
-  my $customer = delete $params{customer};
-  my $employee = delete $params{employee};
+  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';
 
@@ -98,7 +99,7 @@ sub convert_to_sales_order {
       taxzone_id              => $customer->taxzone_id,
       currency_id             => $customer->currency_id,
       transaction_description => $shop->transaction_description,
-      transdate               => DateTime->today_local
+      transdate               => $transdate,
     );
      return $order;
    }else{
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 {
index 1c1bffd..6844b63 100644 (file)
@@ -10,6 +10,7 @@ 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,
@@ -60,6 +61,7 @@ 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;
 }
index 3918ad4..0b14eca 100644 (file)
@@ -56,9 +56,10 @@ sub parse_dbupdate_controls {
     $file =~ s|.*/||;
 
     my $control = {
-      "priority" => 1000,
-      "depends"  => [],
-      "locales"  => [],
+      priority    => 1000,
+      depends     => [],
+      required_by => [],
+      locales     => [],
     };
 
     while (<IN>) {
@@ -71,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 {
@@ -100,7 +101,7 @@ sub parse_dbupdate_controls {
 
     delete @{$control}{qw(depth applied)};
 
-    my @unknown_keys = grep { !m{^ (?: depends | description | file | ignore | locales | may_fail | priority | superuser_privileges | tag ) $}x } keys %{ $control };
+    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)))));
     }
@@ -114,6 +115,15 @@ sub parse_dbupdate_controls {
     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}));
index 6ba89df..9d1bcac 100644 (file)
@@ -8,7 +8,7 @@ our @ISA = qw(Exporter);
 our @EXPORT = qw(conv_i conv_date conv_dateq do_query selectrow_query do_statement
              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
@@ -92,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++;
   }
 
@@ -171,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);
@@ -408,7 +407,7 @@ __END__
 
 =head1 NAME
 
-SL::DBUTils.pm: All about database connections in kivitendo
+SL::DBUtils.pm: All about database connections in kivitendo
 
 =head1 SYNOPSIS
 
@@ -419,6 +418,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)
 
@@ -428,9 +429,11 @@ 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
@@ -454,10 +457,8 @@ not. In most cases you will call it with C<$::form>.
 
 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<<$::form->get_standard_dbh>> to get a generic no_auto connection or get a
-C<Rose::DB::Object> handle from any RDBO class with
-C<<SL::DB::Part->new->db->dbh>>. The former will be without autocommit, the
-latter with autocommit.
+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.
 
@@ -514,7 +515,7 @@ or export only what you need:
   selectall_hashref_query(...)
 
 
-=head2 Peformance
+=head2 Performance
 
 Since it is really easy to write something like
 
@@ -522,7 +523,7 @@ Since it is really easy to write something like
 
 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 shovelled ate 2GB of memory and timeouted the request.
+statement just gobbled up 2GB of memory and timeouted the request.
 
 Parts may be the obvious example, but the same applies to customer, vendors,
 records, projects or custom variables.
@@ -615,6 +616,16 @@ 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, retrieves all data from
@@ -704,6 +715,11 @@ general little need to invoke it manually.
   $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 = ?|;
index 5d0ee14..46ab23a 100644 (file)
--- a/SL/DN.pm
+++ b/SL/DN.pm
@@ -51,6 +51,8 @@ use SL::TransNumber;
 use SL::Util qw(trim);
 use SL::DB;
 
+use File::Copy;
+
 use strict;
 
 sub get_config {
@@ -109,7 +111,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
@@ -118,7 +121,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 {
@@ -126,8 +130,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);
     }
@@ -338,9 +343,19 @@ sub _save_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};
@@ -348,7 +363,8 @@ 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 $next_config_id = conv_i($row->{next_dunning_config_id});
     my $invoice_id     = conv_i($row->{invoice_id});
@@ -358,6 +374,9 @@ sub _save_dunning {
                $next_config_id, $next_config_id);
     do_statement($form, $h_insert_dunning, $q_insert_dunning, @values);
   }
+  # die this transaction, because for this customer only credit notes are
+  # selected ...
+  return unless $customer_id;
 
   $h_update_ar->finish();
   $h_insert_dunning->finish();
@@ -371,6 +390,9 @@ 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_invoices($myconfig, $form, $_, $dbh) for @invoice_ids;
+  }
 
   if ($send_email) {
     $self->send_email($myconfig, $form, $dunning_id, $dbh);
@@ -534,6 +556,11 @@ sub get_invoices {
     push(@values, like($form->{customer}));
   }
 
+  if ($form->{department_id}) {
+    $where .= qq| AND (a.department_id = ?)|;
+    push(@values, $form->{department_id});
+  }
+
   my %columns = (
     "ordnumber" => "a.ordnumber",
     "invnumber" => "a.invnumber",
@@ -566,6 +593,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
@@ -573,6 +601,7 @@ sub get_invoices {
          ct.name AS customername, a.customer_id, a.duedate,
          a.amount - a.paid AS open_amount,
          a.direct_debit,
+         dep.description as departmentname,
 
          cfg.dunning_description, cfg.dunning_level,
 
@@ -585,11 +614,12 @@ 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 dunning_config cfg ON (a.dunning_config_id = cfg.id)
        LEFT JOIN dunning_config nextcfg ON
          (nextcfg.id =
@@ -612,9 +642,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
 
@@ -625,7 +654,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);
   }
@@ -853,6 +882,14 @@ 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,
@@ -902,8 +939,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
@@ -1069,4 +1114,51 @@ sub set_customer_cvars {
 
 }
 
+sub print_original_invoices {
+  my ($self, $myconfig, $form, $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',
+                   },
+  );
+
+  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;
+
+  push @{ $form->{DUNNING_PDFS} }, $file_name;
+  push @{ $form->{DUNNING_PDFS_EMAIL} }, { 'path' => "${spool}/$file_name",
+                                           'name' => $form->get_formname_translation('invoice') . "_" . $invoice->invnumber . ".pdf" };
+
+  $form->{recipient_locale}  = $saved_reicpient_locale;
+}
+
 1;
index 54df47c..034189f 100644 (file)
--- a/SL/DO.pm
+++ b/SL/DO.pm
@@ -129,7 +129,7 @@ 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)) || ($vc eq 'vendor' && $main::auth->assert('purchase_all_edit', 1))) ) {
     push @where, qq|dord.employee_id = (select id from employee where login= ?)|;
     push @values, $::myconfig{login};
   }
@@ -1030,9 +1030,9 @@ sub order_details {
       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"}));
index 6b2e618..849bbda 100644 (file)
@@ -94,15 +94,15 @@ Creates a new vendor.
 
 Minimal usage, default values, without saving to database:
 
-  my $vendor = SL::Dev::CustomerVendor::create_vendor();
+  my $vendor = SL::Dev::CustomerVendor::new_vendor();
 
 Complex usage, overwriting some defaults, and save to database:
 
-  SL::Dev::CustomerVendor::create_vendor(name        => 'Test vendor',
-                                         taxzone_id  => 2,
-                                         notes       => "Order for 100$ for free delivery",
-                                         payment_id  => 5,
-                                        )->save;
+  SL::Dev::CustomerVendor::new_vendor(name        => 'Test vendor',
+                                      taxzone_id  => 2,
+                                      notes       => "Order for 100$ for free delivery",
+                                      payment_id  => 5,
+                                     )->save;
 
 =head1 BUGS
 
index 91759a9..967a1f7 100644 (file)
@@ -50,8 +50,8 @@ sub new_assembly {
   } else {
     for my $i ( 1 .. delete $params{number_of_parts} || 3) {
       my $part = new_part(partnumber  => "$base_partnumber $i",
-                             description => "Testpart $i",
-                            )->save;
+                          description => "Testpart $i",
+                         )->save;
       push( @{$assembly_items}, SL::DB::Assembly->new(parts_id => $part->id,
                                                       qty      => 1,
                                                       position => $i,
@@ -85,8 +85,8 @@ sub new_assortment {
   } else {
     for my $i ( 1 .. delete $params{number_of_parts} || 3) {
       my $part = new_part(partnumber  => "$base_partnumber $i",
-                             description => "Testpart $i",
-                            )->save;
+                          description => "Testpart $i",
+                         )->save;
       push( @{$assortment_items}, SL::DB::AssortmentItem->new(parts_id => $part->id,
                                                               qty      => 1,
                                                               position => $i,
index 0fdb3a4..a093ed4 100644 (file)
@@ -82,6 +82,7 @@ sub create_bank_transaction {
    $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,
index 012b3d2..2f289d0 100644 (file)
@@ -2,7 +2,20 @@ 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);
+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;
@@ -13,7 +26,12 @@ 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',
@@ -300,6 +318,398 @@ sub create_department {
   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__
@@ -442,6 +852,154 @@ 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
 
@@ -449,6 +1007,6 @@ Nothing here yet.
 
 =head1 AUTHOR
 
-G. Richardson E<lt>grichardson@kivitendo-premium.deE<gt>
+G. Richardson E<lt>grichardson@kivitec.deE<gt>
 
 =cut
index fcb3aea..31c76a5 100644 (file)
@@ -19,7 +19,6 @@ use IO::File;
 use List::MoreUtils qw(all);
 use List::Util qw(first);
 use POSIX qw(setlocale);
-use SL::ArchiveZipFixes;
 use SL::Auth;
 use SL::Dispatcher::AuthHandler;
 use SL::LXDebug;
@@ -30,6 +29,7 @@ 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;
 
@@ -50,8 +50,6 @@ 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);
@@ -249,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;
@@ -291,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(
@@ -360,15 +361,104 @@ sub handle_request {
   $::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}");
+  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 {
   my $self = shift;
   return unless $self->_interface_is_fcgi;
@@ -432,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«.
index 182d654..bd3d29e 100644 (file)
@@ -43,12 +43,11 @@ sub handle {
 }
 
 sub _error {
-  my $self = shift;
+  my ($self, %param) = @_;
 
   $::auth->punish_wrong_login;
+  $::dispatcher->handle_login_error(%param, error => 'password');
 
-  require SL::Controller::Base;
-  SL::Controller::Base->new->redirect_to('controller.pl?action=LoginScreen/user_login&error=password');
   return 0;
 }
 
index 8305594..25c6d96 100644 (file)
@@ -4,7 +4,6 @@ use strict;
 
 use parent qw(Rose::Object);
 
-use Clone qw(clone);
 use SL::File::Backend;
 use SL::File::Object;
 use SL::DB::History;
@@ -78,7 +77,7 @@ sub get_all_versions {
       for my $version (2..$maxversion) {
         $main::lxdebug->message(LXDebug->DEBUG2(), "clone for version=".($maxversion-$version+1));
         eval {
-          my $clone = clone($fileobj);
+          my $clone = $fileobj->clone;
           $clone->version($maxversion-$version+1);
           $clone->newest(0);
           $main::lxdebug->message(LXDebug->DEBUG2(), "clone version=".$clone->version." mtime=". $clone->mtime);
index 871b61f..40e1398 100644 (file)
@@ -131,6 +131,8 @@ my %type_to_path = (
   gl_transaction          => 'dialogbuchungen',
   accounts_payable        => 'kreditorenbuchungen',
   shop_image              => 'shopbilder',
+  customer                => 'kunden',
+  vendor                  => 'lieferanten',
 );
 
 my %type_to_model = (
@@ -151,6 +153,8 @@ my %type_to_model = (
   gl_transaction          => 'GLTransaction',
   accounts_payable        => 'GLTransaction',
   shop_image              => 'Part',
+  customer                => 'Customer',
+  vendor                  => 'Vendor',
 );
 
 my %model_to_number = (
@@ -162,6 +166,8 @@ my %model_to_number = (
   Letter          => 'letternumber',
   GLTransaction   => 'reference',
   ShopImage       => 'partnumber',
+  Customer        => 'customernumber',
+  Vendor          => 'vendornumber',
 );
 
 sub webdav_path {
index 7ac8706..3aa553d 100644 (file)
@@ -124,6 +124,10 @@ sub loaded_db_file {  # so, dass wir die nur einmal laden.
   $_[0]->db_file;
 }
 
+sub clone {
+  bless +{ %{ $_[0] } }, __PACKAGE__;
+}
+
 
 sub init_db_file { die 'must always have a db file'; }
 sub init_loaded  { 0 }
index 59d69c3..636eacd 100644 (file)
@@ -47,6 +47,7 @@ use CGI;
 use Cwd;
 use Encode;
 use File::Copy;
+use File::Temp ();
 use IO::File;
 use Math::BigInt;
 use POSIX qw(strftime);
@@ -115,11 +116,6 @@ sub new {
   return $self;
 }
 
-sub read_cgi_input {
-  my ($self) = @_;
-  SL::Request::read_cgi_input($self);
-}
-
 sub _flatten_variables_rec {
   $main::lxdebug->enter_sub(2);
 
@@ -397,7 +393,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);
 
@@ -914,11 +910,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;
 
@@ -935,13 +938,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';
@@ -985,7 +981,6 @@ sub parse_template {
 
   # OUT is used for the media, screen, printer, email
   # for postscript we store a copy in a temporary file
-  my $keep_temp_files = $::lx_office_conf{debug} && $::lx_office_conf{debug}->{keep_temp_files};
 
   my ($temp_fh, $suffix);
   $suffix =  $self->{IN};
@@ -1035,7 +1030,14 @@ sub parse_template {
   }
   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->doc_storage_enabled)
     {
       $self->{attachment_filename} ||= $self->generate_attachment_filename;
@@ -1049,7 +1051,12 @@ 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->doc_storage_enabled) {
     $self->{attachment_filename} ||= $self->generate_attachment_filename;
@@ -1112,8 +1119,8 @@ sub send_email {
   if (($self->{format} eq 'html') && ($self->{sendmode} eq 'inline')) {
     $mail->{content_type}   =  "text/html";
     $mail->{message}        =~ s/\r//g;
-    $mail->{message}        =~ s/\n/<br>\n/g;
-    $full_signature         =~ s/\n/<br>\n/g;
+    $mail->{message}        =~ s{\n}{<br>\n}g;
+    $full_signature         =~ s{\n}{<br>\n}g;
     $mail->{message}       .=  $full_signature;
 
     open(IN, "<", $self->{tmpfile})
@@ -1123,7 +1130,7 @@ sub send_email {
 
   } 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);
+    $attachment_name     =~ s{\.(.+?)$}{.${ext_for_format}} if ($ext_for_format);
 
     if (($self->{attachment_policy} // '') eq 'old_file') {
       my ( $attfile ) = SL::File->get_all(object_id   => $self->{id},
@@ -1263,6 +1270,20 @@ sub get_formname_translation {
   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) = @_;
@@ -1338,6 +1359,10 @@ 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;
 }
@@ -1365,8 +1390,11 @@ sub generate_email_body {
 
   return undef unless $body;
 
-  $body   .= GenericTranslations->get(translation_type =>"salutation_punctuation_mark", language_id => $self->{language_id}) . "\n";
-  $body   .= GenericTranslations->get(translation_type =>"preset_text_$self->{formname}", language_id => $self->{language_id});
+  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               .= GenericTranslations->get(translation_type => "salutation_punctuation_mark",      language_id => $self->{language_id}) . "\n\n";
+  $body               .= $main_body;
 
   $body = $main::locale->unquote_special_chars('HTML', $body);
 
@@ -1860,7 +1888,7 @@ sub add_shipto {
   my @values;
 
   foreach my $item (qw(name department_1 department_2 street zipcode city country gln
-                       contact cp_gender phone fax email)) {
+                       contact phone fax email)) {
     if ($self->{"shipto$item"}) {
       $shipto = 1 if ($self->{$item} ne $self->{"shipto$item"});
     }
@@ -1869,6 +1897,12 @@ sub add_shipto {
 
   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}) {
@@ -1882,10 +1916,10 @@ sub add_shipto {
                      shiptocountry = ?,
                      shiptogln = ?,
                      shiptocontact = ?,
-                     shiptocp_gender = ?,
                      shiptophone = ?,
                      shiptofax = ?,
                      shiptoemail = ?
+                     shiptocp_gender = ?,
                    WHERE shipto_id = ?|;
     do_query($self, $dbh, $query, @values, $self->{shipto_id});
   } else {
@@ -1899,10 +1933,10 @@ sub add_shipto {
                      shiptocountry = ? AND
                      shiptogln = ? AND
                      shiptocontact = ? AND
-                     shiptocp_gender = ? 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);
@@ -1910,7 +1944,7 @@ sub add_shipto {
       my $insert_query =
         qq|INSERT INTO shipto (trans_id, shiptoname, shiptodepartment_1, shiptodepartment_2,
                                shiptostreet, shiptozipcode, shiptocity, shiptocountry, shiptogln,
-                               shiptocontact, shiptocp_gender, shiptophone, shiptofax, shiptoemail, module)
+                               shiptocontact, shiptophone, shiptofax, shiptoemail, shiptocp_gender, module)
            VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)|;
       do_query($self, $dbh, $insert_query, $id, @values, $module);
 
@@ -2058,26 +2092,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();
 
@@ -2117,36 +2131,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();
 
@@ -2360,31 +2344,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;
@@ -2395,10 +2367,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"},
@@ -2417,10 +2385,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"});
   }
@@ -2473,10 +2437,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} });
   }
@@ -2710,7 +2670,7 @@ sub create_links {
   if ($self->{id}) {
     $query =
       qq|SELECT
-           a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid,
+           a.cp_id, a.invnumber, a.transdate, a.${table}_id, a.datepaid, a.deliverydate,
            a.duedate, 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,
@@ -3326,19 +3286,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 then it's
-  # one from the customer's/vendor's master data. Otherwise look an a
-  # customized address linking back to the current record.
-  my $shipto_module = $self->{type} =~ /_delivery_order$/                                             ? 'DO'
-                    : $self->{type} =~ /sales_order|sales_quotation|request_quotation|purchase_order/ ? 'OE'
-                    :                                                                                   'AR';
-  my $shipto        = $self->{shipto_id} ? SL::DB::Shipto->new(shipto_id => $self->{shipto_id})->load
-                    :                      SL::DB::Manager::Shipto->get_first(where => [ module => $shipto_module, trans_id => $self->{id} ]);
-  if ($shipto) {
-    $self->{$_} = $shipto->$_ for grep { m{^shipto} } map { $_->name } @{ $shipto->meta->columns };
-    $self->{"shiptocvar_" . $_->config->name} = $_->value_as_text for @{ $shipto->cvars_by_config };
-  }
-
   my $language = $self->{language} ? '_' . $self->{language} : '';
 
   my ($language_tc, $output_numberformat, $output_dateformat, $output_longdates);
@@ -3407,6 +3354,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),
index cd63ec0..245e5b8 100644 (file)
--- a/SL/GL.pm
+++ b/SL/GL.pm
@@ -123,12 +123,12 @@ sub _post_transaction {
   $query =
     qq|UPDATE gl SET
          reference = ?, description = ?, notes = ?,
-         transdate = ?, department_id = ?, taxincluded = ?,
+         transdate = ?, deliverydate = ?, department_id = ?, taxincluded = ?,
          storno = ?, storno_id = ?, ob_transaction = ?, cb_transaction = ?
        WHERE id = ?|;
 
   @values = ($form->{reference}, $form->{description}, $form->{notes},
-             conv_date($form->{transdate}), conv_i($form->{department_id}), $form->{taxincluded} ? 't' : 'f',
+             conv_date($form->{transdate}), conv_date($form->{deliverydate}), 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',
              conv_i($form->{id}));
   do_query($form, $dbh, $query, @values);
@@ -637,7 +637,8 @@ sub transaction {
 
   if ($form->{id}) {
     $query =
-      qq|SELECT g.reference, g.description, g.notes, g.transdate, g.storno, g.storno_id,
+      qq|SELECT g.reference, g.description, g.notes, g.transdate, g.deliverydate,
+           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
@@ -772,12 +773,22 @@ sub get_chart_balances {
 }
 
 sub get_active_taxes_for_chart {
-  my ($self, $chart_id, $transdate) = @_;
+  my ($self, $chart_id, $transdate, $tax_id) = @_;
 
   my $chart         = SL::DB::Chart->new(id => $chart_id)->load;
   my $active_taxkey = $chart->get_active_taxkey($transdate);
+
+  my $where = [ chart_categories => { like => '%' . $chart->category . '%' } ];
+
+  if ( defined $tax_id && $tax_id >= 0 ) {
+    $where = [ or => [ chart_categories => { like => '%' . $chart->category . '%' },
+                       id               => $tax_id
+                     ]
+             ];
+  }
+
   my $taxes         = SL::DB::Manager::Tax->get_all(
-    where   => [ chart_categories => { like => '%' . $chart->category . '%' }],
+    where   => $where,
     sort_by => 'taxkey, rate',
   );
 
@@ -788,3 +799,48 @@ sub get_active_taxes_for_chart {
 }
 
 1;
+
+__END__
+
+=pod
+
+=encoding utf8
+
+=head1 NAME
+
+SL::GL - some useful GL functions
+
+=head1 FUNCTIONS
+
+=over 4
+
+=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
+
+=head1 BUGS
+
+Nothing here yet.
+
+=head1 AUTHOR
+
+G. Richardson E<lt>grichardson@kivitec.de<gt>
+
+=cut
index 13d5174..cedbb49 100644 (file)
@@ -11,14 +11,17 @@ 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::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,15 +42,22 @@ sub create_pdf {
 sub create_parsed_file {
   my ($class, %params) = @_;
 
-  my $userspath      = $::lx_office_conf{paths}->{userspath};
+  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}       = getcwd();
+  $form->{cwd}       = SL::System::Process::exe_dir();
   $form->{templates} = $::instance_conf->get_templates;
   $form->{IN}        = $params{template};
-  $form->{tmpdir}    = $form->{cwd} . '/' . $userspath;
+  $form->{tmpdir}    = $temp_dir->dirname;
   my $tmpdir         = $form->{tmpdir};
   my ($suffix)       = $params{template} =~ m{\.(.+)};
 
@@ -55,12 +65,22 @@ sub create_parsed_file {
     '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},
@@ -68,6 +88,7 @@ sub create_parsed_file {
     myconfig               => \%::myconfig,
     userspath              => $tmpdir,
     variable_content_types => $params{variable_content_types},
+    %driver_options,
   );
 
   my $result = $parser->parse($temp_fh);
@@ -87,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;
@@ -242,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         => 'ZUGFeRD-invoice.xml',
+        description  => $::locale->text('ZUGFeRD invoice'),
+        relationship => '/Alternative',
+        mime_type    => 'text/xml',
+      }
+    ],
+  );
+
+  return %driver_options;
+}
+
 1;
 __END__
 
diff --git a/SL/Helper/ISO3166.pm b/SL/Helper/ISO3166.pm
new file mode 100644 (file)
index 0000000..35b5d6e
--- /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)$}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)$}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)$}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)$}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;
index 63f637e..c25fd59 100644 (file)
@@ -15,14 +15,20 @@ sub convert_mt940_data {
   $sfile->fh->print($mt940_data);
   $sfile->fh->close;
 
+  # create needed dir structure for aqbanking 5.x and 6.x
   my $todir = $sfile->get_path . '/imexporters/csv/profiles';
   mkpath $todir;
+  die "Cannot create $todir" unless -d $todir;
+
   File::Copy::copy('users/aqbanking.conf', $todir.'/kivi.conf');
+  die "Cannot create local aqbanking conf " unless -f $todir.'/kivi.conf';
+
+  mkpath $sfile->get_path . '/settings6/aqbanking';
 
   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=kivi 2> /dev/null ";
+          $sfile->get_path . "/$import_filename | $aqbin --cfgdir=\"" . $sfile->get_path . "\" export --profile=kivi 2> /dev/null ";
 
   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";'.
index 66fbaf2..be80a69 100644 (file)
@@ -2,6 +2,8 @@ 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 = (
@@ -23,8 +25,9 @@ sub create_massprint_pdf {
   my ($self, %params) = @_;
   my $form = Form->new('');
   my %create_params = (
-      variables => $form,
-      return    => 'file_name',
+    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});
@@ -42,10 +45,29 @@ sub create_massprint_pdf {
   }
 
   $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;
@@ -160,6 +182,15 @@ a tempory $form is used to set
 
 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
 
diff --git a/SL/Helper/UNECERecommendation20.pm b/SL/Helper/UNECERecommendation20.pm
new file mode 100644 (file)
index 0000000..56ea12e
--- /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)$}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;
index 43873e4..2e5f127 100644 (file)
@@ -4,7 +4,7 @@ use strict;
 use parent qw(Rose::Object);
 use version;
 
-use SL::DBUtils qw(selectall_hashref_query selectfirst_hashref_query do_query selectall_ids);
+use SL::DBUtils qw(selectall_hashref_query selectfirst_hashref_query do_query selectcol_array_query);
 use SL::DB;
 
 use Rose::Object::MakeMethods::Generic (
@@ -85,7 +85,7 @@ sub get_all {
 sub get_keys {
   my ($self) = @_;
 
-  my @keys = selectall_ids($::form, $::form->get_standard_dbh, <<"", 0, $self->login, $self->namespace);
+  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;
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/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 a2a0a86..868fb74 100644 (file)
--- a/SL/IC.pm
+++ b/SL/IC.pm
@@ -526,6 +526,22 @@ 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}) {
+    # 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 AND ';
+    @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 .= " $_ ILIKE ? ";
+      push @bind_vars,    like($form->{$_});
+    }
+    $where_clause .='))';
+  }
+
   my $query = <<"  SQL";
     SELECT DISTINCT $select_clause
     FROM parts p
@@ -790,7 +806,8 @@ sub retrieve_accounts {
 SQL
 
   my $query_tax = <<SQL;
-    SELECT c.accno, t.taxdescription AS description, t.rate, t.taxnumber
+    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
@@ -821,7 +838,7 @@ SQL
     $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;
@@ -905,6 +922,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|
@@ -914,7 +950,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}"};
@@ -928,11 +964,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 ]);
index c85daa3..5c768cf 100644 (file)
--- a/SL/IR.pm
+++ b/SL/IR.pm
@@ -549,6 +549,7 @@ SQL
     if ($form->{currency} ne $defaultcurrency) && !$exchangerate;
 
 # record acc_trans transactions
+  my $taxdate = $form->{deliverydate} ? $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);
@@ -575,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);
     }
   }
@@ -730,22 +731,24 @@ 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 = ?,
+                invnumber    = ?, ordnumber   = ?, quonumber     = ?, transdate    = ?,
+                orddate      = ?, quodate     = ?, vendor_id     = ?, amount       = ?,
+                netamount    = ?, paid        = ?, duedate       = ?, deliverydate = ?,
+                invoice      = ?, taxzone_id  = ?, notes         = ?, taxincluded  = ?,
                 intnotes     = ?, storno_id   = ?, storno        = ?,
                 cp_id        = ?, employee_id = ?, department_id = ?, delivery_term_id = ?,
+                payment_id   = ?,
                 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',
          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->{"currency"},
          conv_i($form->{globalproject_id}),
                 $form->{direct_debit} ? 't' : 'f',
@@ -999,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, 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
               FROM ap
               WHERE id = ?|;
   $ref = selectfirst_hashref_query($form, $dbh, $query, conv_i($form->{id}));
@@ -1071,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
@@ -1096,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} ";
       }
 
@@ -1339,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
@@ -1370,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} ";
       }
 
index 97e7248..a99b43f 100644 (file)
--- a/SL/IS.pm
+++ b/SL/IS.pm
@@ -171,7 +171,7 @@ 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);
 
@@ -442,9 +442,9 @@ sub invoice_details {
         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 =
@@ -505,10 +505,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}) {
@@ -1436,7 +1451,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;
@@ -1446,26 +1461,57 @@ 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) {
+      next unless $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);
@@ -2069,7 +2115,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
@@ -2091,7 +2138,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} ";
         }
 
@@ -2393,7 +2441,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
@@ -2422,6 +2470,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} ";
       }
 
index 81d5c91..aeac4e5 100644 (file)
@@ -18,7 +18,8 @@ BEGIN {
 @required_modules = (
   { name => "parent",                              url => "http://search.cpan.org/~corion/",    debian => 'libparent-perl' },
   { name => "Algorithm::CheckDigits",              url => "http://search.cpan.org/~mamawe/",    debian => 'libalgorithm-checkdigits-perl' },
-  { name => "Archive::Zip",    version => '1.16',  url => "http://search.cpan.org/~phred/",     debian => 'libarchive-zip-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' },
@@ -30,7 +31,7 @@ BEGIN {
   { 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 => "Digest::SHA",                         url => "http://search.cpan.org/~mshelor/",   debian => 'libdigest-sha-perl' },
-  { name => "Exception::Class",                    url => "https://metacpan.org/pod/Exception::Class", debian => 'libexception-class-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' },
@@ -62,6 +63,7 @@ BEGIN {
   { 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 => "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' },
 );
index 72b3802..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>
@@ -240,7 +248,7 @@ Returns the configuration for experimental feature "assortment"
 
 =item C<get_feature_experimental_order>
 
-Returns the configuration for experimental feature "order"
+Returns the configuration for the experimental feature "order"
 
 =item C<get_parts_show_image>
 
@@ -252,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 6b27b0a..0503dcb 100644 (file)
@@ -161,6 +161,7 @@ sub clone_for_dump {
   my ($src, $dumped) = @_;
 
   return undef if !defined($src);
+  return $src  if !ref($src);
 
   $dumped ||= {};
   my $addr  = refaddr($src);
index eb9f7a2..d609e3e 100644 (file)
@@ -151,8 +151,6 @@ sub _create_attachment_part {
   my $file_id       = 0;
   my $email_journal = $::instance_conf->get_email_journal;
 
-  $::lxdebug->message(LXDebug->DEBUG2(), "mail5 att=" . $attachment . " email_journal=" . $email_journal . " id=" . $attachment->{id});
-
   if (ref($attachment) eq "HASH") {
     $attributes{filename}     = $attachment->{name};
     $file_id                  = $attachment->{id}   || '0';
@@ -177,8 +175,6 @@ sub _create_attachment_part {
   $attachment_content ||= ' ';
   $attributes{charset}  = $self->{charset} if $self->{charset} && ($attributes{content_type} =~ m{^text/});
 
-  $::lxdebug->message(LXDebug->DEBUG2(), "mail6 mtype=" . $attributes{content_type} . " filename=" . $attributes{filename});
-
   my $ent;
   if ( $attributes{content_type} eq 'message/rfc822' ) {
     $ent = Email::MIME->new($attachment_content);
@@ -252,11 +248,12 @@ sub send {
   # Set defaults & headers
   $self->{charset}        =  'UTF-8';
   $self->{content_type} ||=  "text/plain";
-  $self->{headers}        =  [
+  $self->{headers}      ||=  [];
+  push @{ $self->{headers} }, (
     Subject               => $self->{subject},
     'Message-ID'          => '<' . $self->_create_message_id . '>',
     'X-Mailer'            => "kivitendo " . SL::Version->get_version,
-  ];
+  );
   $self->{mail_attachments} = [];
   $self->{content_by_name}  = $::instance_conf->get_email_journal == 1 && $::instance_conf->get_doc_files;
 
@@ -268,10 +265,6 @@ sub send {
 
     my $email = $self->_create_message;
 
-    #$::lxdebug->message(0, "message: " . $email->as_string);
-    # return "boom";
-
-    $::lxdebug->message(LXDebug->DEBUG2(), "mail1 from=".$self->{from}." to=".$self->{to});
     my $from_obj = (Email::Address->parse($self->{from}))[0];
 
     $self->{driver}->start_mail(from => $from_obj->address, to => [ $self->_all_recipients ]);
index 74c9ffd..974a1a1 100644 (file)
--- a/SL/OE.pm
+++ b/SL/OE.pm
@@ -100,6 +100,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
 
@@ -115,8 +121,10 @@ sub transactions {
     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|  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, | .
@@ -134,6 +142,7 @@ 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 | .
     qq|WHERE (o.quotation = ?) |;
   push(@values, $quotation);
@@ -181,7 +190,7 @@ SQL
     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)) || ($vc eq 'vendor' && $main::auth->assert('purchase_all_edit', 1))) ) {
     $query .= " AND o.employee_id = (select id from employee where login= ?)";
     push @values, $::myconfig{login};
   }
@@ -353,6 +362,7 @@ SQL
     "insertdate"              => "o.itime",
     "taxzone"                 => "tz.description",
     "payment_terms"           => "pt.description",
+    "department"              => "department.description",
   );
   if ($form->{sort} && grep($form->{sort}, keys(%allowed_sort_columns))) {
     $sortorder = $allowed_sort_columns{$form->{sort}} . " ${sortdir}"  . ", o.itime ${sortdir}";
@@ -368,9 +378,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};
   }
@@ -596,6 +612,9 @@ sub _save {
         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,
@@ -606,7 +625,7 @@ sub _save {
         );
         $new_project->save;
         $form->{"globalproject_id"} = $new_project->id;
-      };
+      }
 
       CVar->get_non_editable_ic_cvars(form               => $form,
                                       dbh                => $dbh,
@@ -1080,6 +1099,7 @@ sub _retrieve {
     my $transdate = $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
@@ -1102,8 +1122,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}
@@ -1177,8 +1197,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) | .
@@ -1196,6 +1217,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} ";
         }
 
@@ -1512,9 +1534,9 @@ sub order_details {
         # 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, | .
@@ -1573,10 +1595,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);
index 145d912..bc5ee8a 100644 (file)
@@ -241,6 +241,10 @@ 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.
+
 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.
index 80dca58..3129437 100644 (file)
@@ -11,7 +11,7 @@ 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 restricted_html textarea_tag link_tag date_tag
-);
+  div_tag);
 our %EXPORT_TAGS = (ALL => \@EXPORT_OK);
 
 use Carp;
@@ -354,6 +354,11 @@ sub date_tag {
   );
 }
 
+sub div_tag {
+  my ($content, %params) = @_;
+  return html_tag('div', $content, %params);
+}
+
 1;
 __END__
 
index 3b03f5a..75180a9 100644 (file)
@@ -165,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;
     }
index 75cac69..c33b6b3 100644 (file)
@@ -10,15 +10,16 @@ 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                  => [ qw(applying_database_upgrades) ],
+  scalar                  => [ qw(applying_database_upgrades post_data) ],
   'scalar --get_set_init' => [ qw(cgi layout presenter is_ajax type) ],
 );
 
@@ -232,6 +233,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) = @_;
@@ -270,7 +277,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
@@ -279,29 +286,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 %$temp_target;
+  _recode_recursively($iconv, $data_to_decode, $target) if keys %$data_to_decode;
 
   if ($target->{RESTORE_FORM_FROM_SESSION_ID}) {
     my %temp_form;
@@ -381,10 +397,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);
@@ -564,6 +580,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 373711d..fc92bcf 100644 (file)
@@ -29,13 +29,6 @@ sub retrieve_open_invoices {
 
   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 =
@@ -63,7 +56,7 @@ 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)
 
@@ -591,7 +584,3 @@ Returns undef if the deletion was successfully.
 Otherwise the function just dies with a short notice of the id.
 
 =cut
-
-
-
-
index e410e73..a37cd03 100644 (file)
@@ -11,6 +11,7 @@ use Rose::Object::MakeMethods::Generic (
 use File::Slurp;
 use File::Spec::Functions qw(:ALL);
 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
 #
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..8682c7e 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,7 +46,7 @@ sub available_templates {
   my @alldir  = sort grep {
        -d ($::lx_office_conf{paths}->{templates} . "/$_")
     && !/^\.\.?$/
-    && !m/\.(?:html|tex|sty|odt|xml|txb)$/
+    && !m/\.(?:html|tex|sty|odt)$/
     && !m/^(?:webpages$|print$|mail$|\.)/
   } keys %dir_h;
 
index 6a5d8e4..b2b6838 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',
@@ -388,33 +390,76 @@ sub _parse_config_lines {
   }
 }
 
+sub _embed_file_directive {
+  my ($self, $file) = @_;
+
+  # { source      => $xmlfile,
+  #   name        => 'ZUGFeRD-invoice.xml',
+  #   description => $::locale->text('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 {
@@ -431,7 +476,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);
 
@@ -476,12 +521,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 : $!";
@@ -535,7 +589,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 : $!";
@@ -595,11 +649,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,,
   );
 
@@ -608,7 +663,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;
 
@@ -619,7 +674,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;
index b38365b..7916b78 100644 (file)
@@ -82,6 +82,7 @@ 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 _set_id_attribute {
   my ($attributes, $name, $unique) = @_;
@@ -118,11 +119,6 @@ sub radio_button_tag {
   return $code;
 }
 
-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);
@@ -304,9 +300,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
   }
 
@@ -566,9 +566,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
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 5c320fc..ee2d21f 100644 (file)
 
 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;
@@ -419,89 +422,6 @@ 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();
 
@@ -543,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;
@@ -609,6 +526,9 @@ sub 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,
@@ -616,7 +536,7 @@ sub ustva {
       attribute   => 'position',
       dec_places  => '2',
   });
-
+  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);
   }
@@ -627,7 +547,7 @@ sub ustva {
       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;
@@ -654,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
@@ -677,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
   #
@@ -781,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
@@ -802,7 +722,7 @@ sub get_accounts_ustva {
        )
        WHERE
        $acc_trans_where
-       GROUP BY tk.pos_ustva
+       GROUP BY tk.pos_ustva, t.rate, c.accno
     |;
 
   } elsif ($form->{accounting_method} eq 'accrual') {
@@ -814,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
@@ -829,7 +750,7 @@ 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 {
@@ -847,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
@@ -864,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
@@ -887,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
@@ -911,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");
@@ -926,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;
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 3ec6e08..5a3db9c 100644 (file)
--- a/SL/VK.pm
+++ b/SL/VK.pm
@@ -56,7 +56,7 @@ 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|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|RIGHT JOIN ar on (i.trans_id = ar.id) | .
@@ -85,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;
index 9186072..158b216 100644 (file)
--- a/SL/WH.pm
+++ b/SL/WH.pm
 
 package WH;
 
+use Carp qw(croak);
+
 use SL::AM;
 use SL::DBUtils;
+use SL::DB::Inventory;
 use SL::Form;
+use SL::Locale::String qw(t8);
 use SL::Util qw(trim);
 
 use warnings;
@@ -56,7 +60,6 @@ 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 ($now)      = selectrow_query($::form, $::form->get_standard_dbh, qq|SELECT current_date|);
@@ -807,8 +810,9 @@ 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';
@@ -1126,6 +1130,31 @@ $main::lxdebug->enter_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__
@@ -1279,6 +1308,16 @@ The typical params would be:
     '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
index 946f2bf..c513925 100644 (file)
@@ -32,6 +32,8 @@ my %type_to_path = (
   letter                  => 'briefe',
   general_ledger          => 'dialogbuchungen',
   accounts_payable        => 'kreditorenbuchungen',
+  customer                => 'kunden',
+  vendor                  => 'lieferanten',
 );
 
 sub get_all_files {
diff --git a/SL/X.pm b/SL/X.pm
index 756793b..9343dea 100644 (file)
--- a/SL/X.pm
+++ b/SL/X.pm
@@ -22,11 +22,14 @@ use Exception::Class (
   'SL::X::DBRoseError'  => {
     isa                 => 'SL::X::DBError',
     fields              => [ qw(class metaobject object) ],
-    defaults            => { error_template => [ '\'%s\' in object of type \'%s\' occured', qw(db_error class) ] },
+    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',
+  },
 );
 
 1;
diff --git a/SL/ZUGFeRD.pm b/SL/ZUGFeRD.pm
new file mode 100644 (file)
index 0000000..780122c
--- /dev/null
@@ -0,0 +1,219 @@
+package SL::ZUGFeRD;
+
+use strict;
+use warnings;
+use utf8;
+
+use CAM::PDF;
+use Data::Dumper;
+use List::Util qw(first);
+use XML::LibXML;
+
+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;
+
+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 = '2p0';
+      last;
+    }
+
+    if ($ns->getData =~ m{zugferd}i) {
+      $zugferd_version = 'unsupported';
+      last;
+    }
+  }
+
+  if (!$zugferd_version) {
+    return {
+      result  => RES_ERR_NOT_ZUGFERD(),
+      message => $::locale->text('The XMP metadata does not declare the ZUGFeRD data.'),
+    };
+  }
+
+  if ($zugferd_version !~ m{^2p}) {
+    return {
+      result  => RES_ERR_UNSUPPORTED_ZUGFERD_VERSION(),
+      message => $::locale->text('The 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 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 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 ZUGFeRD
+invoice data from it. First it'll parse the XMP metadata and look for
+the 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 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 65afb3b..507e645 100644 (file)
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-3.5.4
+3.5.6.1
index fa7f092..d041561 100644 (file)
@@ -661,6 +661,9 @@ sub config {
 
   $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();
 
   $myconfig{show_form_details} = 1 unless (defined($myconfig{show_form_details}));
   $form->{CAN_CHANGE_PASSWORD} = $main::auth->can_change_password();
@@ -1222,7 +1225,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();
 }
index 49f6f7a..2a031bd 100644 (file)
@@ -47,6 +47,7 @@ use SL::DB::BankTransactionAccTrans;
 use SL::DB::Chart;
 use SL::DB::Currency;
 use SL::DB::Default;
+use SL::DB::Order;
 use SL::DB::PurchaseInvoice;
 use SL::DB::RecordTemplate;
 use SL::DB::Tax;
@@ -427,8 +428,7 @@ sub form_header {
                                     "old_id"    => \@old_project_ids },
                    "charts"    => { "key"       => "ALL_CHARTS",
                                     "transdate" => $form->{transdate} },
-                   "taxcharts" => { "key"       => "ALL_TAXCHARTS",
-                                    "module"    => "AP" },);
+                  );
 
   map(
     { $_->{link_split} = [ split(/:/, $_->{link}) ]; }
@@ -460,9 +460,6 @@ sub form_header {
   my $follow_up_trans_info =  "$form->{invnumber} ($follow_up_vc)";
 
   $::request->layout->add_javascripts("autocomplete_chart.js", "show_vc_details.js", "show_history.js", "follow_up.js", "kivi.Draft.js", "kivi.GL.js", "kivi.RecordTemplate.js", "kivi.File.js", "kivi.AP.js", "kivi.CustomerVendor.js", "kivi.Validator.js");
-  my $transdate = $::form->{transdate} ? DateTime->from_kivitendo($::form->{transdate}) : DateTime->today_local;
-  my $first_taxchart;
-
   # $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.
@@ -471,6 +468,12 @@ sub form_header {
   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}) {
 
@@ -479,9 +482,13 @@ sub form_header {
     $form->{"tax_$i"} = $form->format_amount(\%myconfig, $form->{"tax_$i"}, 2);
 
     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, $transdate);
 
+    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;
@@ -500,7 +507,7 @@ sub form_header {
     my $item = shift;
     return [
       $item->{id} .'--'. $item->{rate},
-      $item->{taxdescription} .' '. ($item->{rate} * 100) .' %',
+      $item->{taxkey} . ' - ' . $item->{taxdescription} .' '. ($item->{rate} * 100) .' %',
     ];
   };
 
@@ -649,7 +656,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++;
@@ -899,7 +905,7 @@ sub use_as_new {
 
   $main::auth->assert('ap_transactions');
 
-  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);
+  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}--;
 
@@ -1001,12 +1007,12 @@ sub ap_transactions {
 
   my @columns =
     qw(transdate id type invnumber ordnumber name netamount tax amount paid datepaid
-       due duedate transaction_description notes employee globalprojectnumber
+       due duedate transaction_description notes employee globalprojectnumber department
        vendornumber country ustid taxzone payment_terms charts direct_debit);
 
   my @hidden_variables = map { "l_${_}" } @columns;
   push @hidden_variables, "l_subtotal", qw(open closed vendor invnumber ordnumber transaction_description notes project_id transdatefrom transdateto
-                                           parts_partnumber parts_description);
+                                           parts_partnumber parts_description department_id);
 
   my $href = build_std_url('action=ap_transactions', grep { $form->{$_} } @hidden_variables);
 
@@ -1028,6 +1034,7 @@ 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.'), },
@@ -1037,7 +1044,7 @@ sub ap_transactions {
     'direct_debit'            => { 'text' => $locale->text('direct debit'), },
   );
 
-  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)) {
     my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
     $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
   }
@@ -1054,10 +1061,13 @@ 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 @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')              . " : $form->{department}"                     if ($form->{department});
+  push @options, $locale->text('Department')              . " : $department_description"                 if ($form->{department_id});
   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});
@@ -1183,6 +1193,74 @@ 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->{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 = @_;
 
index 15e756e..4481939 100644 (file)
@@ -378,9 +378,6 @@ sub form_header {
   $form->{forex}        = $form->check_exchangerate( \%myconfig, $form->{currency}, $form->{transdate}, 'buy');
   $form->{exchangerate} = $form->{forex} if $form->{forex};
 
-  # format exchangerate
-  $form->{exchangerate}    = $form->{exchangerate} ? $form->format_amount(\%myconfig, $form->{exchangerate}) : '';
-
   $rows = max 2, $form->numtextrows($form->{notes}, 50);
 
   my @old_project_ids = grep { $_ } map { $form->{"project_id_$_"} } 1..$form->{rowcount};
@@ -390,8 +387,7 @@ 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;
 
@@ -416,11 +412,14 @@ sub form_header {
   my $follow_up_trans_info =  "$form->{invnumber} ($follow_up_vc)";
 
   $::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");
-
-  my $transdate = $::form->{transdate} ? DateTime->from_kivitendo($::form->{transdate}) : DateTime->today_local;
+  # 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"},
@@ -431,14 +430,18 @@ sub form_header {
     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;
 
-    foreach my $item ( GL->get_active_taxes_for_chart($amount_chart_id, $transdate) ) {
+    my $used_tax_id;
+    if ( $form->{"taxchart_$i"} ) {
+      ($used_tax_id) = split(/--/, $form->{"taxchart_$i"});
+    }
+    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->taxdescription . " " . $item->rate * 100 . ' %';
+      $taxchart_labels{$key} = $item->taxkey . " - " . $item->taxdescription . " " . $item->rate * 100 . ' %';
     }
 
     $taxchart_to_use    //= $default_taxchart // $first_taxchart;
@@ -1064,7 +1067,7 @@ sub ar_transactions {
     %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 name datepaid employee shippingpoint shipvia transaction_description direct_debit department)) {
     my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
     $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
   }
index c16f6c3..3124362 100644 (file)
@@ -103,8 +103,10 @@ sub add {
 
   DN->get_config(\%myconfig, \%$form);
 
+  $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');
 
@@ -207,7 +209,7 @@ sub save_dunning {
 
   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}) {
@@ -221,9 +223,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"}, };
     }
 
@@ -246,6 +250,7 @@ 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};
@@ -362,6 +367,7 @@ 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') },
index fd4dd2c..bd906d4 100644 (file)
@@ -336,6 +336,7 @@ sub setup_do_action_bar {
           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,
         ],
       ], # end of combobox "Export"
 
@@ -465,11 +466,6 @@ sub form_header {
 
   $::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));
 
-  my @custom_hidden;
-  push @custom_hidden, map { "shiptocvar_" . $_->name } @{ SL::DB::Manager::CustomVariableConfig->get_all(where => [ module => 'ShipTo' ]) };
-
-  $::form->{HIDDENS} = [ map { +{ name => $_, value => $::form->{$_} } } (@custom_hidden) ];
-
   setup_do_action_bar();
 
   $form->header();
@@ -500,8 +496,15 @@ sub form_footer {
   $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();
 }
index 0261ef1..3481fca 100644 (file)
@@ -18,6 +18,7 @@ my %mail_strings = (
   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'),
@@ -210,6 +211,48 @@ sub save_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 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 ZUGFeRD notes have been saved.');
+
+  edit_zugferd_notes();
+}
+
 sub setup_generictranslations_edit_greetings_action_bar {
   my %params = @_;
 
@@ -237,6 +280,7 @@ sub setup_generictranslations_edit_sepa_strings_action_bar {
     );
   }
 }
+
 sub setup_generictranslations_edit_email_strings_action_bar {
   my %params = @_;
 
@@ -251,4 +295,18 @@ sub setup_generictranslations_edit_email_strings_action_bar {
   }
 }
 
+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 8f51232..fe62112 100644 (file)
@@ -802,6 +802,7 @@ sub display_rows {
   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}) {
@@ -825,14 +826,20 @@ sub display_rows {
     $accno_id    = $chart->{id};
     my ($first_taxchart, $default_taxchart, $taxchart_to_use);
 
-    foreach my $item ( GL->get_active_taxes_for_chart($accno_id, $transdate) ) {
+    my $used_tax_id;
+    if ( $form->{"taxchart_$i"} ) {
+      ($used_tax_id) = split(/--/, $form->{"taxchart_$i"});
+    }
+
+    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->taxdescription . " " . $item->rate * 100 . ' %';
+      $taxchart_labels{$key} = $item->taxkey . " - " . $item->taxdescription . " " . $item->rate * 100 . ' %';
     }
 
     $taxchart_to_use    //= $default_taxchart // $first_taxchart;
@@ -1482,8 +1489,9 @@ sub continue {
 }
 
 sub get_tax_dropdown {
-  my $transdate    = $::form->{transdate} ? DateTime->from_kivitendo($::form->{transdate}) : DateTime->today_local;
-  my @tax_accounts = GL->get_active_taxes_for_chart($::form->{accno_id}, $transdate);
+  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 });
 
   print $::form->ajax_response_header, $html;
index 1c8145c..ae158c7 100644 (file)
@@ -40,7 +40,7 @@
 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;
@@ -56,6 +56,7 @@ use SL::PriceSource;
 use SL::Presenter::Part;
 
 use SL::DB::Contact;
+use SL::DB::Currency;
 use SL::DB::Customer;
 use SL::DB::Default;
 use SL::DB::Language;
@@ -371,8 +372,9 @@ sub display_row {
       }
     }
 
-    my $edit_prices     = $main::auth->assert('edit_prices', 1) && (!$::form->{"active_price_source_$i"} || !$price || $price->editable);
-    my $edit_discounts  = $main::auth->assert('edit_prices', 1) && !$::form->{"active_discount_source_$i"};
+    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);
@@ -909,16 +911,24 @@ sub order {
   _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->globalproject_id(undef) if !$order->globalproject_id;
-    $order->payment_id(undef)       if !$order->payment_id;
+
+    $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;
-      $item->discount($item->discount/100.0);
 
       # 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).
@@ -934,6 +944,7 @@ sub order {
 
     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();
@@ -1287,6 +1298,10 @@ sub print_form {
     $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!'))
     if ($form->{media} eq 'email');
   $form->isblank("${inv}date",
@@ -1656,7 +1671,7 @@ sub relink_accounts {
   $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"} = "";
 
@@ -1847,7 +1862,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',
@@ -1895,12 +1910,19 @@ sub _make_record_item {
       if ($obj->meta->column($method)->isa('Rose::DB::Object::Metadata::Column::Date')) {
         $obj->${\"$method\_as_date"}($value);
       } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::(?:Numeric|Float|DoublePrecsion)$/) {
-        $obj->${\"$method\_as_number"}($value);
+        $obj->${\"$method\_as_number"}(($value // '') eq '' ? undef : $value);
       } elsif ((ref $obj->meta->column($method)) =~ /^Rose::DB::Object::Metadata::Column::Boolean$/) {
         $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($value);
       }
+
+      if ($method eq 'discount') {
+        $obj->discount($obj->discount / 100.0);
+      }
+
     } else {
       $obj->{__additional_form_attributes}{$method} = $value;
     }
@@ -1910,6 +1932,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;
 }
 
@@ -1929,6 +1956,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;
@@ -1946,9 +1975,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});
     }
@@ -1957,12 +1988,21 @@ 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::Invoice') {
+    my $paid = $factor *
+      sum
+      map  { $::form->parse_amount(\%::myconfig, $::form->{$_}) }
+      grep { m{^paid_\d+$} }
+      keys %{ $::form };
+    $obj->paid($paid);
+  }
+
   return $obj;
 }
 
@@ -1971,7 +2011,7 @@ sub setup_sales_purchase_print_options {
   $print_form->{printers}  = SL::DB::Manager::Printer->get_all_sorted;
   $print_form->{languages} = SL::DB::Manager::Language->get_all_sorted;
 
-  $print_form->{$_} = $::form->{$_} for qw(type media language_id printer_id storno);
+  $print_form->{$_} = $::form->{$_} for qw(type media language_id printer_id storno formname groupitems);
 
   return SL::Helper::PrintOptions->get_print_options(
     form    => $print_form,
@@ -2050,11 +2090,20 @@ sub show_sales_purchase_email_dialog {
 
   $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 $email_form = {
     to                  => $email,
     cc                  => $email_cc,
     subject             => $::form->generate_email_subject,
-    message             => $::form->generate_email_body('record_email' => $record_email),
+    message             => $::form->generate_email_body(%body_params),
     attachment_filename => $::form->generate_attachment_filename,
     js_send_function    => 'kivi.SalesPurchase.send_email()',
   };
@@ -2097,3 +2146,36 @@ sub send_sales_purchase_email {
 
   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         => 'ZUGFeRD-invoice.xml',
+        description  => $::locale->text('ZUGFeRD invoice'),
+        relationship => '/Alternative',
+        mime_type    => 'text/xml',
+      }
+    ];
+  };
+
+  if (my $e = SL::X::ZUGFeRDValidation->caught) {
+    $::form->error($e->message);
+  }
+}
index dd4f113..b84516b 100644 (file)
@@ -451,7 +451,7 @@ sub form_header {
     shiptoemail shiptodepartment_1 shiptodepartment_2 message email subject cc bcc taxaccounts cursor_fokus
     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->{duedate})->to_kivitendo if $TMPL_VAR{payment_terms_obj};
index 9baad4d..c1b61d5 100644 (file)
@@ -536,7 +536,7 @@ sub form_header {
     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.Draft kivi.File kivi.SalesPurchase kivi.Part kivi.CustomerVendor kivi.Validator ckeditor/ckeditor ckeditor/adapters/jquery kivi.io client_js));
 
@@ -727,10 +727,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"};
   }
index 0121b93..8fd9c38 100644 (file)
@@ -56,6 +56,8 @@ 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}       = '_corona' if (DateTime->today->month >= 7 && DateTime->today->year == 2020
+                                      && DateTime->today->month <= 11);
 
   # create the logo screen
   $form->header() unless $form->{noheader};
index c62e694..012914d 100644 (file)
@@ -419,8 +419,9 @@ sub setup_oe_action_bar {
         ],
         action => [
           t8('E Mail'),
-          call   => [ 'kivi.SalesPurchase.show_email_dialog' ],
-          checks => [ 'kivi.validate_form' ],
+          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'),
@@ -651,7 +652,7 @@ sub form_header {
         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->{$_} = $type_check_vars{$_} for keys %type_check_vars;
 
@@ -1026,7 +1027,7 @@ sub orders {
     "curr",                    "employee",
     "salesman",
     "shipvia",                 "globalprojectnumber",
-    "transaction_description", "open",
+    "transaction_description", "department",            "open",
     "delivered",               "periodic_invoices",
     "marge_total",             "marge_percent",
     "vcnumber",                "ustid",
@@ -1111,6 +1112,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'), },
@@ -1129,7 +1131,7 @@ sub orders {
     %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)) {
     my $sortdir                 = $form->{sort} eq $name ? 1 - $form->{sortdir} : $form->{sortdir};
     $column_defs{$name}->{link} = $href . "&sort=$name&sortdir=$sortdir";
   }
@@ -2169,18 +2171,18 @@ sub edit_periodic_invoices_config {
   $config = SL::YAML::Load($::form->{periodic_invoices_config}) if $::form->{periodic_invoices_config};
 
   if ('HASH' ne ref $config) {
-    my $lang_id = $::form->{language_id};
     $config =  { periodicity             => 'm',
                  order_value_periodicity => 'p', # = same as periodicity
                  start_date_as_date      => $::form->{transdate} || $::form->current_date,
                  extend_automatically_by => 12,
                  active                  => 1,
-                 email_subject           => GenericTranslations->get(language_id => $lang_id,
-                                              translation_type =>"preset_text_periodic_invoices_email_subject"),
-                 email_body              => GenericTranslations->get(language_id => $lang_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->{lanuage_id},
+                                              translation_type =>"preset_text_periodic_invoices_email_subject");
+  $config->{email_body}    ||= GenericTranslations->get(language_id => $::form->{lanuage_id},
+                                              translation_type =>"preset_text_periodic_invoices_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);
@@ -2194,6 +2196,7 @@ sub edit_periodic_invoices_config {
 
   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);
index 7218846..b78083e 100755 (executable)
@@ -102,10 +102,11 @@ 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 =
index 49f4727..0f26de3 100644 (file)
@@ -30,14 +30,6 @@ 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;
@@ -245,32 +237,6 @@ sub report {
 }
 
 
-
-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();
 
@@ -501,12 +467,6 @@ sub ustva_vorauswahl {
   return $select_vorauswahl;
 }
 
-#sub config {
-#  $::lxdebug->enter_sub();
-#  config_step1();
-#  $::lxdebug->leave_sub();
-#}
-
 sub show_options {
   $::lxdebug->enter_sub();
 
@@ -554,9 +514,6 @@ 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;
-
 
   my $ustva = USTVA->new();
   $ustva->get_config();
@@ -636,42 +593,10 @@ sub generate_ustva {
     $form->{co_city} =~ s/\\n//g;
   }
 
-  ################################
-  #
-  # Nation specific customisations
-  #
-  ################################
-
-  # Germany
-
-  if ( $form->{coa} eq 'Germany-DATEV-SKR03EU' or $form->{coa} eq 'Germany-DATEV-SKR04EU') {
-
    $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 (@{$::form->{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' ) ? ',':'.';
-
-      foreach my $number (@{$::form->{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;";
@@ -687,41 +612,11 @@ sub generate_ustva {
       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' ) . "!");
-      $::dispatcher->end_request;
-
-    } else { # All other Formats are wrong
+   } else { # we have only html
       $form->header;
       USTVA::error( $locale->text('Application Error. Wrong Format') . ": " . $form->{format} );
       $::dispatcher->end_request;
-    }
-
-
-  } else  # Outputformat for generic output
-  {
-
-    $form->{USTVA} = [];
-
-    if ( $form->{format} eq 'generic') { # Formatierungen für HTML Ausgabe
-
-      my $rec_ref = {};
-      for my $kennziffer (@{$::form->{category_cent}}, @{$::form->{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;
-      }
-
-    }
-
-  }
-
+   }
   if ( $form->{period} eq '13' and $form->{format} ne 'html') {
     $form->header;
     USTVA::info(
@@ -730,31 +625,12 @@ sub generate_ustva {
       . '!');
   }
 
-  $form->{templates} = "doc" if ( $form->{type} eq 'help' );
-
-  if ($form->{format} eq 'generic'){
-
-    $form->header();
+  # 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');
 
-    my $template_ref = {
-        taxnumber => $defaults->taxnumber,
-    };
-
-    print($form->parse_html_template('ustva/generic_taxreport', $template_ref));
-
-  } elsif ( $form->{format} eq 'elstertaxbird' ) {
-   $form->parse_template(\%myconfig);
-  } 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();
 }
index ea05ab4..8a0dc46 100644 (file)
@@ -137,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 };
@@ -171,6 +171,7 @@ sub invoice_transactions {
     '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'), },
@@ -209,6 +210,7 @@ sub invoice_transactions {
   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('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};
index 78b0576..1f55444 100644 (file)
@@ -791,7 +791,7 @@ sub generate_report {
   my $sort_col     = $form->{sort};
 
   my %filter;
-  my @columns = qw(warehousedescription bindescription partnumber type_and_classific partdescription chargenumber bestbefore comment qty partunit  purchase_price 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 classification_id partnumber description chargenumber bestbefore date include_invalid_warehouses);
@@ -835,7 +835,7 @@ sub generate_report {
   $form->{report_generator_output_format} = 'HTML' if !$form->{report_generator_output_format};
 
   # manual paginating
-  my $allrows        = !!($form->{report_generator_output_format} ne 'HTML') ;
+  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;
@@ -864,7 +864,7 @@ sub generate_report {
   my @hidden_variables = map { "l_${_}" } @columns;
   push @hidden_variables, qw(warehouse_id bin_id partnumber partstypes_id description 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);
+  push @hidden_variables, qw(classification_id stock_value_basis);
 
   my %column_defs = (
     'warehousedescription' => { 'text' => $locale->text('Warehouse'), },
@@ -878,6 +878,7 @@ sub generate_report {
     '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 $href = build_std_url('action=generate_report', grep { $form->{$_} } @hidden_variables);
@@ -885,7 +886,7 @@ sub generate_report {
 
   map { $column_defs{$_}->{link} = $href . "&page=".$page."&sort=${_}&order=" . Q($_ eq $sort_col ? 1 - $form->{order} : $form->{order}) } @columns;
 
-  my %column_alignment = map { $_ => 'right' } qw(qty purchase_price stock_value);
+  my %column_alignment = map { $_ => 'right' } qw(qty list_price purchase_price stock_value);
 
   map { $column_defs{$_}->{visible} = $form->{"l_${_}"} ? 1 : 0 } @columns;
 
@@ -927,6 +928,7 @@ sub generate_report {
 #                                                       'conv_units' => 'convertible');
     $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 } ];
 
@@ -941,6 +943,7 @@ sub generate_report {
 #                                                               'conv_units' => 'convertible');
       $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;
 
index 0715dc0..cf29984 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,6 +74,8 @@ base_dn       =
 filter        =
 bind_dn       =
 bind_password =
+timeout       = 10
+verify        = require
 
 [system]
 # Set language for login and admin forms. Currently "de" (German)
@@ -189,6 +206,11 @@ openofficeorg_daemon_port = 2002
 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
index 8611462..0c22493 100644 (file)
@@ -531,6 +531,15 @@ span.toggle_selected {
     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,
index 8cf9a67..a2a4ad0 100644 (file)
@@ -525,6 +525,15 @@ a.red {
     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,
index c9cbf58..5ee9ab3 100644 (file)
@@ -2,8 +2,62 @@ Wichtige Hinweise zum Upgrade von älteren Versionen
 ===================================================
 
 
+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.
+
+
 ** BITTE FERTIGEN SIE VOR DEM UPGRADE EIN BACKUP IHRER DATENBANK(EN) AN! **
 
+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*"
+
+Folgender sed-Einzeiler erledigt das:
+
+ sed -i 's/ignore: 0/ignore: 1/g' sql/Pg-upgrade2/konjunkturpaket_2020*
+
+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
 
index 0799a5f..1e796f8 100644 (file)
@@ -2,6 +2,146 @@
 # Veränderungen von kivitendo #
 ###############################
 
+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
 
 
@@ -26,10 +166,12 @@ Kleinere neue Features und Detailverbesserungen:
   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 allgemeine E-Mail-Adresse identisch ist. Falls dieses Feld gesetzt ist, so hat dieser
+  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!).
+  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.
index 6917944..46a0461 100644 (file)
@@ -2,7 +2,7 @@
 <!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.5.4: Installation, Konfiguration,
+  <title>kivitendo 3.5.6.1: Installation, Konfiguration,
   Entwicklung</title>
 
   <chapter id="Aktuelle-Hinweise">
@@ -13,7 +13,7 @@
     <itemizedlist>
       <listitem>
         <para>im Community-Forum: <ulink
-        url="https://forum.kivitendo.de:32443">https://forum.kivitendo.de:32443</ulink></para>
+        url="https://forum.kivitendo.de">https://forum.kivitendo.de</ulink></para>
       </listitem>
 
       <listitem>
         ohne große Probleme auf den derzeit aktuellen verbreiteten
         Distributionen läuft.</para>
 
-        <para>Anfang 2019 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>
 
             <itemizedlist>
               <listitem>
-                <para>8.0 "Jessie"</para>
-              </listitem>
-              <listitem>
-                <para>9.0 "Stretch"</para>
+                <para>10.0 "Buster"</para>
               </listitem>
               <listitem>
-                <para>10.0 "Buster"</para>
+                <para>11.0 "Bullseye"</para>
               </listitem>
             </itemizedlist>
           </listitem>
 
           <listitem>
-            <para>16.04 "Xenial Xerus" LTS und 18.04 "Bionic Beaver" LTS
+            <para>20.04 "Focal Fossa" LTS
           </para>
           </listitem>
 
           <listitem>
-            <para>openSUSE 15.0</para>
+            <para>openSUSE Leap 15.x und SUSE Linux Enterprise Server 15 GA</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>
             <para><literal>XML::Writer</literal></para>
           </listitem>
 
+          <listitem>
+            <para><literal>XML::LibXML</literal></para>
+          </listitem>
+
           <listitem>
             <para><literal>YAML::XS</literal> oder <literal>YAML</literal></para>
           </listitem>
         </itemizedlist>
 
+
+        <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>,
   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
+  libdatetime-event-cron-perl libexception-class-perl libcam-pdf-perl \
+  libxml-libxml-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>
-          <note id="build-essential">
-          <para>Für ältere Ubuntu/Debians müßen einige Pakete per CPAN installiert werden.
-          Das geht bspw. für das benötige Paket HTML::Restrict mit:</para>
-          <programlisting>apt-get install build-essential
-cpan HTML::Restrict</programlisting>
-          </note>
         </sect3>
 
         <sect3>
@@ -464,14 +465,15 @@ cpan HTML::Restrict</programlisting>
         </sect3>
 
         <sect3>
-          <title>openSUSE</title>
+          <title>openSUSE Leap 15.x und SUSE Linux Enterprise Server 15 GA</title>
 
-          <para>Für openSUSE stehen die meisten der benötigten Perl-Pakete als RPM-Pakete zur Verfügung.</para>
-          <para>Dies setzt voraus, das eben die erforderlichen Repositories dem System bekannt gemacht worden sind.</para>
-          <para>Um zusätzliche 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. Da wahrscheinlich für die Administration eine SSH-Verbindung zum Server benutzt wird.</para>
+          <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.0/ \
+  http://download.opensuse.org/repositories/devel:languages:perl/openSUSE_Leap_15.2/ \
   "devel:languages:perl"
           </programlisting>
           <programlisting>zypper addrepo -f \
@@ -488,12 +490,14 @@ cpan HTML::Restrict</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.0 unterstützt. Die SuSE Macher haben ab Version 15.0 einen größen Umbau in der Verwaltung der Pakete vorgenommen, das heißt, der Paketumfang ist der SLES 15 als kleinsten Nenner angepasst. Dies gilt besonders der openSUE Distribution. Es gibt ja einmal die openSUSE Distri und die Professionelle SLES Version. Dadurch sind viele Pakete aus dem Repositorie enfernt worden, aber auch viele auf aktuellen Stand gehalten.</para>
+          <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>Das überprüfen wir mit YaST. Sollte openSUSE bis zur Version 15.0 zum Einsatz kommen und der Administrator bei der Installation der Distribution die KDE Oberfläche aktiviert hat, loggen wir uns am Server direkt ein, starten das Verwaltungsprogram in einer Konsole wie folgt:</para>
+          <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>Sie können mit folgendem Befehl installiert werden:</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>
 
@@ -527,7 +531,7 @@ cpan HTML::Restrict</programlisting>
   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 plazieren, der Vollständigkeit halber hier für die Installation mit angegeben.
+          <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 \
@@ -542,7 +546,7 @@ cpan HTML::Restrict</programlisting>
           </programlisting>
 
           <para>Zusätzlich müssen einige Pakete aus dem CPAN installiert
-          werden. Dazu können Sie die folgenden Befehle nutzen:</para>
+          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>
@@ -554,7 +558,7 @@ cpan HTML::Restrict</programlisting>
 
         <itemizedlist>
       <listitem>
-            <para><literal>aqbanking-tools</literal> Für das Parsen des MT940 Bankformats</para>
+            <para><literal>aqbanking-tools</literal> Für das Parsen des MT940 Bankformats (Version 6 oder höher)</para>
           </listitem>
           <listitem>
             <para><literal>poppler-utils</literal> 'pdfinfo' zum Erkennen der Seitenanzahl bei der PDF-Generierung</para>
@@ -644,24 +648,30 @@ git checkout `git tag -l | egrep -ve "(alpha|beta|rc)" | tail -1`</programlistin
         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 der aktuelle release, den wir wollen
-$ git add templates/fullhouse    # das sind unsere druckvorlagen inkl. produktbilder
-$ git commit -m "juhu tolle ändernungen"
+$ 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/fullhouse/img/webdav/tesla.png
- create mode 100644 templates/fullhouse/mahnung.tex
- create mode 100644 templates/fullhouse/zahlungserinnerung_zwei.tex
- create mode 100644 templates/fullhouse/zahlungserinnerung_zwei_invoice.tex
+ 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
 
-$ git fetch
-$ git rebase --onto release-3.5.0 release-3.4.1 meine_aenderungen
 Zunächst wird der Branch zurückgespult, um Ihre Änderungen
 darauf neu anzuwenden ...
-Wende an: juhu tolle ändernungen
-$ service apache2 restart
+Wende an: juhu tolle änderungen
+$ service apache2 restart                   # webserver starten!
 </programlisting></para>
       </note>
     </sect1>
@@ -1121,6 +1131,10 @@ Alias /kivitendo-erp/ /var/www/kivitendo-erp/
             <listitem>
               <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>Als Perl Backend wird das Modul <filename>FCGI.pm</filename>
@@ -1261,6 +1275,26 @@ Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</prog
 
        <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>
@@ -1275,6 +1309,19 @@ Alias       /url/for/kivitendo-erp-fcgid/          /path/to/kivitendo-erp/</prog
         url="https://mozilla.github.io/server-side-tls/ssl-config-generator/">
         SSL-Konfigurations-Generator</ulink>.</para>
       </sect2>
+      <sect3>
+        <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>
+      </sect3>
+
     </sect1>
 
     <sect1 id="config.task-server">
@@ -1480,7 +1527,7 @@ systemctl enable kivitendo-task-server.service</programlisting>
           <listitem>
             <para><literal>status</literal> berichtet, ob der Task-Server
             läuft.</para>
-          </listitem>
+          </listitem>yy
         </itemizedlist>
 
         <para>Der Task-Server wechselt beim Starten automatisch in das
@@ -1493,8 +1540,59 @@ systemctl enable kivitendo-task-server.service</programlisting>
         so startet dieser nach Beendigung automatisch erneut.</para>
 
       </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>
+<literal>Daten:</literal><programlisting>multiplier: 100
+digits_year: 4</programlisting>
+          </listitem>
+        </itemizedlist>
+     </sect2>
+    </sect1>
     <sect1 id="Benutzerauthentifizierung-und-Administratorpasswort">
       <title>Benutzerauthentifizierung und Administratorpasswort</title>
 
@@ -1514,9 +1612,8 @@ systemctl enable kivitendo-task-server.service</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
@@ -1599,22 +1696,28 @@ systemctl enable kivitendo-task-server.service</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>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>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>Die verfügbaren Parameter für die LDAP-Konfiguration lauten:</para>
 
         <variablelist>
           <varlistentry>
@@ -1645,6 +1748,17 @@ systemctl enable kivitendo-task-server.service</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>
 
@@ -1694,6 +1808,14 @@ systemctl enable kivitendo-task-server.service</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>
 
@@ -2067,24 +2189,23 @@ systemctl enable kivitendo-task-server.service</programlisting>
   texlive-collection-latexrecommended texlive-collection-langgerman \
   texlive-collection-langenglish</programlisting></para>
 
+      <note>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: <programlisting>scripts/installations_check.pl -l</programlisting> </note>
       <para>kivitendo bringt drei alternative Vorlagensätze mit:</para>
 
       <itemizedlist>
         <listitem>
           <para>RB</para>
         </listitem>
-
         <listitem>
-          <para>f-tex</para>
+          <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.6 entfernt, da er nicht mehr gepflegt wird.</para>
 
       <sect2 id="Vorlagenverzeichnis-anlegen"
              xreflabel="Vorlagenverzeichnis anlegen">
@@ -2108,7 +2229,7 @@ systemctl enable kivitendo-task-server.service</programlisting>
           <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>RB</filename>, <filename>marei</filename> oder
             <filename>odt-rev</filename>.)</para>
           </listitem>
 
@@ -2166,247 +2287,7 @@ systemctl enable kivitendo-task-server.service</programlisting>
         </itemizedlist>
       </sect2>
 
-      <sect2 id="f-tex">
-        <title>f-tex</title>
-
-        <para>Ein Vorlagensatz, der in wenigen Minuten alle Dokumente zur
-        Verfügung stellt.</para>
-
-        <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>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>Hintergrund-PDF umschaltbar auf "nur erste Seite"
-              (Standard) oder "alle Seiten" (Option
-              "<option>bgPdfFirstPageOnly</option>" in Datei
-              <filename>letter.lco</filename>)</para>
-            </listitem>
-
-            <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>
-
-            <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>
-
-            <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>Multisprachfähig leicht um weitere Sprachen zu erweitern,
-              alle Übersetzungen in der Datei
-              <filename>translatinos.tex</filename>.</para>
-            </listitem>
-
-            <listitem>
-              <para>Auflistung von Bruttopreisen für Endverbraucher.</para>
-            </listitem>
-          </itemizedlist>
-        </sect3>
-
-        <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>
-
-            <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>oder etwas detaillierter:</para>
-
-          <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>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>
-
-          <para>Letzlich muss <filename>letter_head.pdf</filename> auf das
-          passende Hintergrund-PDF verweisen, welches gewünschten Briefkopf
-          enthält.</para>
-
-          <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">
+     <sect2 id="Vorlagen-rev-odt">
         <title>Der Druckvorlagensatz rev-odt</title>
 
         <para>Hierbei handelt es sich um einen Dokumentensatz der mit
@@ -4516,6 +4397,15 @@ systemctl enable kivitendo-task-server.service</programlisting>
               </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>
 
@@ -5296,7 +5186,7 @@ systemctl enable kivitendo-task-server.service</programlisting>
               <term><varname>longdescription</varname></term>
 
               <listitem>
-                <para>Langtext</para>
+                <para>Langtext, vorbelegt mit dem Feld Bemerkungen der entsprechenden Ware</para>
               </listitem>
             </varlistentry>
 
@@ -6415,6 +6305,21 @@ Beschreibung: &lt;%description%&gt;
         <para>Der Befehl <command>&lt;bullet&gt;</command> funktioniert
         momentan auch nur in Latex-Vorlagen.</para>
       </sect2>
+
+      <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>
+
     </sect1>
 
     <sect1 id="excel-templates">
@@ -7222,6 +7127,148 @@ document_path = /var/local/kivi_documents
         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
+
+        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 &quot;$@&quot;
+        ------------------------------------------------------------
+
+        4. In config/kivitendo.conf den Parameter »latex« auf den Pfad zu »run_pdflatex.sh« setzen
+
+        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>https://www.ferd-net.de/standards/was-ist-zugferd/index.html</ulink>
+      </para>
+               </sect2>
+
+               <sect2 id="features.zugferd.create_zugferd_bills">
+
+                       <title>Erstellen von ZUGFeRD Rechnungen in Kivitendo</title>
+                       <para>Für die Erstellung von ZUGFeRD Rechnungen bedarf es in
+                       kivitendo zwei Dinge:</para>
+
+                       <orderedlist>
+
+                               <listitem>
+                                       <para>Die Erstellung muss in der Mandantenkonfiguration
+                                       aktiviert sein</para>
+                               </listitem>
+
+                               <listitem>
+                                       <para>Beim mindestens einem Bankkonto muss die Option
+                                       „Nutzung von ZUGFeRD“ aktiviert sein</para>
+                               </listitem>
+
+                       </orderedlist>
+                       <sect3>
+                               <title>Mandantenkonfiguration</title>
+
+                               <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>
+
+                               <itemizedlist>
+                                       <listitem>
+                                               <para>ZUGFeRD-Rechnungen erzeugen</para>
+                                       </listitem>
+
+                                       <listitem>
+                                               <para>ZUGFeRD-Rechnungen im Testmodus erzeugen</para>
+                                       </listitem>
+
+                                       <listitem>
+                                               <para>Keine ZUGFeRD Rechnungen erzeugen</para>
+                                       </listitem>
+
+                               </itemizedlist>
+
+                               <para>Rechnungen die als PDF erzeugt werden, werden je nach
+                               Einstellung nun im ZUGFeRD Format ausgegeben.</para>
+
+                       </sect3>
+
+                       <sect3>
+                               <title>Konfiguration der Bankkonten</title>
+
+                               <para>Unter „System → Bankkonten“ muss bei mindestens einem
+                               Bankkonto die Option „Nutzung mit ZUGFeRD“ auf „Ja“ gestellt
+                               werden.</para>
+                       </sect3>
+
+               </sect2>
+
+               <sect2 id="features.zugferd.read_zugferd_bills">
+
+                       <title>Einlesen von ZUGFeRD Rechnungen in Kivitendo</title>
+
+                       <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>
+
+                       <orderedlist>
+
+                               <listitem>
+                                       <para>Beim Lieferanten muss die Umsatzsteuer-ID und das
+                                       Bankkonto hinterlegt sein</para>
+                               </listitem>
+
+                               <listitem>
+                                       <para>Für den Kreditoren muss eine Buchungsvorlage existieren.</para>
+                               </listitem>
+
+                       </orderedlist>
+
+                       <para>Wenn diese Voraussetzungen erfüllt sind, kann die Rechnung
+                       über „Finanzbuchhaltung“ → „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>
+
+       </sect1>
+
   </chapter>
 
   <chapter>
@@ -8410,7 +8457,7 @@ 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
               example MS Windows doesn't like ':' in its files where Linux
               doesn't mind that. If you want the files created with your
index 9f59e59..a3616d3 100644 (file)
@@ -1,9 +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.5.4: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.5.4: Installation, Konfiguration, Entwicklung"><link rel="prev" href="index.html" title="kivitendo 3.5.4: 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:32443" target="_top">https://forum.kivitendo.de: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.5.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.5.6.1: Installation, Konfiguration, Entwicklung"><link rel="prev" href="index.html" title="kivitendo 3.5.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.5.4: Installation, Konfiguration,
+            </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.5.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 8ff7423..95b370f 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.5.4: 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
+   <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.5.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.5.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>
index 8495fbe..998fd65 100644 (file)
@@ -1,13 +1,12 @@
 <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.5.4: 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.5.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 2019 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>8.0 "Jessie"</p></li><li class="listitem"><p>9.0 "Stretch"</p></li><li class="listitem"><p>10.0 "Buster"</p></li></ul></div></li><li class="listitem"><p>16.04 "Xenial Xerus" LTS und 18.04 "Bionic Beaver" LTS
-          </p></li><li class="listitem"><p>openSUSE 15.0</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
+        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
@@ -17,6 +16,8 @@
                      <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">URI</code>
                   </p></li><li class="listitem"><p>
                      <code class="literal">XML::Writer</code>
+                  </p></li><li class="listitem"><p>
+                     <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.5.3 sind die folgenden Pakete hinzugekommen: <code class="literal">Exception::Class</code>
+                  </p></li></ul></div><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>
         
         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="d0e548"></a>2.2.2.1. Debian und Ubuntu</h4></div></div></div><p>Für Debian und Ubuntu stehen die meisten der benötigten
+        installieren.</p><div class="sect3" title="2.2.2.1. Debian und Ubuntu"><div class="titlepage"><div><div><h4 class="title"><a name="d0e565"></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 \
   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
+  libdatetime-event-cron-perl libexception-class-perl libcam-pdf-perl \
+  libxml-libxml-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 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="build-essential"></a>Anmerkung</th></tr><tr><td align="left" valign="top"><p>Für ältere Ubuntu/Debians müßen einige Pakete per CPAN installiert werden.
-          Das geht bspw. für das benötige Paket HTML::Restrict mit:</p><pre class="programlisting">apt-get install build-essential
-cpan HTML::Restrict</pre></td></tr></table></div></div><div class="sect3" title="2.2.2.2. Fedora"><div class="titlepage"><div><div><h4 class="title"><a name="d0e568"></a>2.2.2.2. Fedora</h4></div></div></div><p>Für Fedora stehen die meisten der benötigten Perl-Pakete als
+                  </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="d0e580"></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 \
@@ -172,8 +175,8 @@ cpan HTML::Restrict</pre></td></tr></table></div></div><div class="sect3" title=
   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-libwww-perl</pre></div><div class="sect3" title="2.2.2.3. openSUSE"><div class="titlepage"><div><div><h4 class="title"><a name="d0e575"></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.</p><p>Dies setzt voraus, das eben die erforderlichen Repositories dem System bekannt gemacht worden sind.</p><p>Um zusätzliche 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. Da wahrscheinlich für die Administration eine SSH-Verbindung zum Server benutzt wird.</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.0/ \
+  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="d0e587"></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/\
@@ -181,7 +184,7 @@ cpan HTML::Restrict</pre></td></tr></table></div></div><div class="sect3" title=
           </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.0 unterstützt. Die SuSE Macher haben ab Version 15.0 einen größen Umbau in der Verwaltung der Pakete vorgenommen, das heißt, der Paketumfang ist der SLES 15 als kleinsten Nenner angepasst. Dies gilt besonders der openSUE Distribution. Es gibt ja einmal die openSUSE Distri und die Professionelle SLES Version. Dadurch sind viele Pakete aus dem Repositorie enfernt worden, aber auch viele auf aktuellen Stand gehalten.</p></td></tr></table></div><p>Das überprüfen wir mit YaST. Sollte openSUSE bis zur Version 15.0 zum Einsatz kommen und der Administrator bei der Installation der Distribution die KDE Oberfläche aktiviert hat, loggen wir uns am Server direkt ein, starten das Verwaltungsprogram 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>Sie 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 \
+          </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 \
@@ -205,7 +208,7 @@ cpan HTML::Restrict</pre></td></tr></table></div></div><div class="sect3" title=
   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 plazieren, der Vollständigkeit halber hier für die Installation mit angegeben.
+          </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 \
@@ -216,9 +219,9 @@ cpan HTML::Restrict</pre></td></tr></table></div></div><div class="sect3" title=
   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 nutzen:</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="d0e631"></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">aqbanking-tools</code> Für das Parsen des MT940 Bankformats</p></li><li class="listitem"><p>
+          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="d0e649"></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">aqbanking-tools</code> Für das Parsen des MT940 Bankformats (Version 6 oder höher)</p></li><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 aqbanking-tools postgresql-contrib poppler-utils</pre><p>
             </p><p>Fedora: </p><pre class="programlisting">dnf install aqbanking poppler-utils postgresql-contrib</pre><p>
index eb973dd..24953ac 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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
@@ -40,23 +40,29 @@ git checkout `git tag -l | egrep -ve "(alpha|beta|rc)" | tail -1`</pre><p>
         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 der aktuelle release, den wir wollen
-$ git add templates/fullhouse    # das sind unsere druckvorlagen inkl. produktbilder
-$ git commit -m "juhu tolle ändernungen"
+$ 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/fullhouse/img/webdav/tesla.png
- create mode 100644 templates/fullhouse/mahnung.tex
- create mode 100644 templates/fullhouse/zahlungserinnerung_zwei.tex
- create mode 100644 templates/fullhouse/zahlungserinnerung_zwei_invoice.tex
+ 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
 
-$ git fetch
-$ git rebase --onto release-3.5.0 release-3.4.1 meine_aenderungen
 Zunächst wird der Branch zurückgespult, um Ihre Änderungen
 darauf neu anzuwenden ...
-Wende an: juhu tolle ändernungen
-$ service apache2 restart
+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 c98d965..78332a8 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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
index 4f3d88d..f2e5d6d 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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.
+   <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.5.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>
index d4b169a..ef46abc 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.5.4: 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="d0e1111"></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.5.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="d0e1129"></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
@@ -41,7 +41,7 @@ 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.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></ul></div><p>Als Perl Backend wird das Modul <code class="filename">FCGI.pm</code>
+          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
@@ -104,16 +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. Authentifizierung mittels HTTP Basic Authentication"><div class="titlepage"><div><div><h3 class="title"><a name="d0e1262"></a>2.6.3. Authentifizierung mittels HTTP Basic Authentication</h3></div></div></div><p>
+          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="d0e1283"></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. Weitergehende Konfiguration"><div class="titlepage"><div><div><h3 class="title"><a name="d0e1278"></a>2.6.4. Weitergehende Konfiguration</h3></div></div></div><p>Für einen deutlichen Sicherheitsmehrwert sorgt die Ausführung
+       </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="d0e1299"></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="d0e1313"></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><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
+        SSL-Konfigurations-Generator</a>.</p></div><div class="sect3" title="2.6.1. Aktivierung von Apache2 modsecurity"><div class="titlepage"><div><div><h4 class="title"><a name="d0e1324"></a>2.6.1. Aktivierung von Apache2 modsecurity</h4></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 a1fc6b8..07d7e2a 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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
@@ -44,7 +44,7 @@
         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.3.1. SystemV-basierende Systeme (z.B. ältere Debian, ältere openSUSE, ältere Fedora)"><div class="titlepage"><div><div><h4 class="title"><a name="d0e1352"></a>2.7.3.1. SystemV-basierende Systeme (z.B. ältere Debian, ältere
+        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="d0e1397"></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
           <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
 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="d0e1381"></a>2.7.3.2. Upstart-basierende Systeme (z.B. Ubuntu bis 14.04)</h4></div></div></div><p>Kopieren Sie die Datei
+          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="d0e1426"></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.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="d0e1399"></a>2.7.3.3. systemd-basierende Systeme (z.B. neure openSUSE, neuere
+          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="d0e1444"></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
@@ -86,4 +86,32 @@ systemctl enable kivitendo-task-server.service</pre><p>Wenn Sie den 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><p>Wurde der Task-Server als systemd-Service eingerichtet (s.o.),
-        so startet dieser nach Beendigung automatisch erneut.</p></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
+        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><code class="literal">Daten:</code><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 9919e5b..a3a801c 100644 (file)
@@ -1,15 +1,14 @@
 <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.5.4: 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.5.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
                      <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 Passwö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
@@ -85,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 7bc7aaf..f35a350 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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>
index e8ddd69..6c27406 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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
index 77cb227..e8b474b 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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,
+   <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.5.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:
index 381ee77..d64d4d9 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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.
+   <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.5.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>
@@ -14,8 +14,8 @@
             </p><pre class="programlisting">zypper install texlive-collection-latex texlive-collection-latexextra \
   texlive-collection-latexrecommended texlive-collection-langgerman \
   texlive-collection-langenglish</pre><p>
-         </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
+         </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">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: <pre class="programlisting">scripts/installations_check.pl -l</pre></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.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>
@@ -24,7 +24,7 @@
         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">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
             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
+            "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,
         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.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
+        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
index 0e1a7e0..74498e0 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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
       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 class="sect2" title="2.13.1. OpenDocument (odt) Druckvorlagen mit Makros"><div class="titlepage"><div><div><h3 class="title"><a name="d0e2449"></a>2.13.1. OpenDocument (odt) Druckvorlagen mit Makros</h3></div></div></div><p>OpenDocument Vorlagen können Makros enthalten, welche komplexere
+      ü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="d0e2420"></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="d0e2462"></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="d0e2467"></a>2.13.1.2. Vorbereitungen im Adminbereich</h4></div></div></div><p>Damit beim Erstellen von Rechnungen und Aufträgen neben der
+        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="d0e2433"></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="d0e2438"></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
               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="d0e2491"></a>2.13.1.3. Benutzereinstellungen</h4></div></div></div><p>Wer den Ausdruck mit Einzahlungsschein als Standardeinstellung
+              "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="d0e2462"></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="d0e2515"></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
+              (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="d0e2486"></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
               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="d0e2579"></a>2.13.1.5. Auswahl der Druckvorlage in kivitendo beim Erzeugen einer
+              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="d0e2550"></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="d0e2584"></a>2.13.1.6. Makroeinstellungen in LibreOffice anpassen</h4></div></div></div><p>Falls beim Öffnen einer von kivitendo erzeugten odt-Rechnung
+          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="d0e2555"></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
index 7fd48df..7d77338 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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
+   <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.5.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
index 5420629..7bc1388 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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:
+   <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.5.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
index 1279263..43f551d 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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
+   <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.5.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
index f8c31c8..a8d5e94 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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
+   <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.5.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
index 1b3f6b9..c4619ce 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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
+   <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.5.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
index 96dc62c..6f26dc4 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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
index 2e77a80..a18d95b 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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> →
index 9015d54..8d9668f 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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>
index 8dbf621..189e2c9 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.5.4: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.5.4: 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
+   <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.5.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.5.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
index 9a1b088..a329014 100644 (file)
@@ -1,6 +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.5.4: 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
+   <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.5.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 14eddb3..fc05d06 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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.5.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
                      </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">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>
                         <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="d0e5851"></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="d0e5833"></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:
@@ -782,4 +786,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 da92c89..c500985 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.5.4: 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.5.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 9f2b656..d6b03c7 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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.
index 5b1c22e..8c8a352 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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
+   <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.5.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
index b98809c..15f53f5 100644 (file)
@@ -1,15 +1,15 @@
 <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.5.4: 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="d0e6519"></a>3.7.1. Übersicht</h3></div></div></div><p>Die Klassifizierung von Artikeln dient einer weiteren
+   <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.5.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="d0e6515"></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="d0e6524"></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
+        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="d0e6520"></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="d0e6554"></a>3.7.3. Attribute</h3></div></div></div><p>Bisher haben die Klassifizierungen folgende Attribute, die auch
+        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="d0e6550"></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
@@ -19,7 +19,7 @@
         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="d0e6585"></a>3.7.4. Zwei-Zeichen Abkürzung</h3></div></div></div><p>Der Typ des Artikels und die Klassifizierung werden durch zwei
+        &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="d0e6581"></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)
index 4f3bbff..e938f7d 100644 (file)
@@ -1,10 +1,10 @@
 <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.5.4: 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="d0e6597"></a>3.8.1. Übersicht</h3></div></div></div><p>Parallel zum alten WebDAV gibt es ein Datei-Management-System,
+   <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.5.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="d0e6593"></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="d0e6624"></a>3.8.2. Struktur</h3></div></div></div><p>Über eine vom Speichermedium unabhängige Zwischenschicht werden
+            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="d0e6620"></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
@@ -23,7 +23,7 @@
         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="d0e6676"></a>3.8.3. Anwendung</h3></div></div></div><p>Die Daten werden bei den ERP-Objekten als extra Reiter
+        <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="d0e6672"></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
         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="d0e6719"></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="d0e6725"></a>3.8.4.1.1. Reiter "Features"</h5></div></div></div><p>Unter dem Reiter <span class="bold"><strong>Features</strong></span>
+        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="d0e6715"></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="d0e6721"></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="d0e6741"></a>3.8.4.1.2. Reiter "Allgemeine Dokumentenanhänge"</h5></div></div></div><p>Unter dem Reiter <span class="bold"><strong>Allgemeine
+            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="d0e6737"></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
index 2aa8122..e58e3b1 100644 (file)
@@ -1,13 +1,13 @@
 <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.5.4: 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="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.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="ch04.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="d0e6775"></a>3.9. Webshop-Api</h2></div></div></div><p>Das Shopmodul bietet die Möglichkeit Onlineshopartikel und
+   <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.5.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="d0e6771"></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="d0e6784"></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="d0e6799"></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="d0e6807"></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="d0e6810"></a>3.9.3.1. Shopvariablenreiter in Artikelstammdaten</h4></div></div></div><p>Mit dem Recht "Shopartikel anlegen und bearbeiten" und des
+      werden.</p><div class="sect2" title="3.9.1. Rechte für die Webshopapi"><div class="titlepage"><div><div><h3 class="title"><a name="d0e6780"></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="d0e6795"></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="d0e6803"></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="d0e6806"></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
           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="d0e6823"></a>3.9.3.2. Shopartikelliste</h4></div></div></div><p>Unter dem Menu Webshop-&gt;Webshop Artikel hat man nochmal
+          zuzuordenen</p></div><div class="sect3" title="3.9.3.2. Shopartikelliste"><div class="titlepage"><div><div><h4 class="title"><a name="d0e6819"></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="d0e6831"></a>3.9.4. Bestellimport</h3></div></div></div><p>Unter dem Menupunkt Webshop-&gt;Webshop Import öffnet sich die
+          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="d0e6827"></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
             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="d0e6884"></a>3.9.5. Mapping der Daten</h3></div></div></div><p>Das Mapping der kivitendo Daten mit den Shopdaten geschieht in
+            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="d0e6880"></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="ch04.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;Kapitel 4. Entwicklerdokumentation</td></tr></table></div></body></html>
\ No newline at end of file
+            </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..e3aaa24
--- /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.5.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="" 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="d0e6927"></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="d0e6944"></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“ → „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 549cb8f..f78a81c 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.5.4: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.5.4: Installation, Konfiguration, Entwicklung"><link rel="prev" href="ch03s09.html" title="3.9. Webshop-Api"><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="ch03s09.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="d0e6894"></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="d0e6900"></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.5.6.1: Installation, Konfiguration, Entwicklung"><link rel="up" href="index.html" title="kivitendo 3.5.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="d0e6963"></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="d0e6969"></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="d0e7001"></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="d0e7070"></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="d0e7034"></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="d0e7103"></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="d0e7098"></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="d0e7167"></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="d0e7182"></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="d0e7251"></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 Datumsformat dateformat und
           ü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="d0e7221"></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="d0e7290"></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="d0e7239"></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="d0e7308"></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="d0e7276"></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="d0e7345"></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
@@ -144,7 +144,7 @@ $main::lxdebug-&gt;message(0, 'Wer bin ich? Kunde oder Lieferant:' . $form-&gt;{
           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="d0e7305"></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="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="d0e7374"></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
@@ -154,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="d0e7341"></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="d0e7410"></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="d0e7362"></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="d0e7431"></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="d0e7380"></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="d0e7449"></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
@@ -176,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="d0e7422"></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="d0e7427"></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="d0e7491"></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="d0e7496"></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="d0e7443"></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="d0e7512"></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="d0e7462"></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="d0e7531"></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="ch03s09.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.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;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 681c059..05631d5 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.5.4: 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
+   <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.5.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>,
index cc7e228..6544093 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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>
+   <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.5.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
index f890f54..bfb2b08 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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
+   <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.5.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
index 9431196..59bda4a 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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
+   <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.5.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
@@ -54,7 +54,7 @@ Template/HTML
 Template/XML
 Template/LaTeX
 Template/OpenDocument
-filenames</pre><p>The last of which is very machine dependant. Remember that
+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
index ce23337..7927ef2 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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
+   <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.5.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>
index 40cb168..47a6550 100644 (file)
@@ -1,6 +1,6 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <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.5.4: 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-
+   <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.5.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
index 83ea464..ced49d0 100644 (file)
@@ -1,6 +1,6 @@
 <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.5.4: 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>™
+   <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.5.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
index f680552..6eef76b 100644 (file)
@@ -1,9 +1,9 @@
 <html><head>
       <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
-   <title>kivitendo 3.5.4: 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.5.4: 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.5.4: 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.5.4: Installation, Konfiguration, Entwicklung"><div class="titlepage"><div><div><h1 class="title"><a name="kivitendo-documentation"></a>kivitendo 3.5.4: 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#d0e631">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#d0e1111">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#d0e1262">2.6.3. Authentifizierung mittels HTTP Basic Authentication</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#d0e1278">2.6.4. 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#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></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><dd><dl><dt><span class="sect2"><a href="ch02s13.html#d0e2449">2.13.1. OpenDocument (odt) Druckvorlagen mit Makros</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:
+   <title>kivitendo 3.5.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.5.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.5.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.5.6.1: Installation, Konfiguration, Entwicklung"><div class="titlepage"><div><div><h1 class="title"><a name="kivitendo-documentation"></a>kivitendo 3.5.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#d0e649">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#d0e1129">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#d0e1283">2.6.3. Authentifizierung mittels HTTP Basic Authentication</a></span></dt><dt><span class="sect2"><a href="ch02s06.html#d0e1299">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#d0e1313">2.6.5. 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#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#d0e2420">2.13.1. OpenDocument (odt) Druckvorlagen mit Makros</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><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#d0e6519">3.7.1. Übersicht</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6524">3.7.2. Basisklassifizierung</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6554">3.7.3. Attribute</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6585">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#d0e6597">3.8.1. Übersicht</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e6624">3.8.2. Struktur</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e6676">3.8.3. Anwendung</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e6719">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#d0e6784">3.9.1. Rechte für die Webshopapi</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e6799">3.9.2. Konfiguration</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e6807">3.9.3. Webshopartikel</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e6831">3.9.4. Bestellimport</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e6884">3.9.5. Mapping der Daten</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#d0e6900">4.1.1. Wie sehen globale Variablen in Perl aus?</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7001">4.1.2. Warum sind globale Variablen ein Problem?</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7034">4.1.3. Kanonische globale Variablen</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7422">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
+        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#d0e6515">3.7.1. Übersicht</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6520">3.7.2. Basisklassifizierung</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6550">3.7.3. Attribute</a></span></dt><dt><span class="sect2"><a href="ch03s07.html#d0e6581">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#d0e6593">3.8.1. Übersicht</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e6620">3.8.2. Struktur</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e6672">3.8.3. Anwendung</a></span></dt><dt><span class="sect2"><a href="ch03s08.html#d0e6715">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#d0e6780">3.9.1. Rechte für die Webshopapi</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e6795">3.9.2. Konfiguration</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e6803">3.9.3. Webshopartikel</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e6827">3.9.4. Bestellimport</a></span></dt><dt><span class="sect2"><a href="ch03s09.html#d0e6880">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#d0e6969">4.1.1. Wie sehen globale Variablen in Perl aus?</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7070">4.1.2. Warum sind globale Variablen ein Problem?</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7103">4.1.3. Kanonische globale Variablen</a></span></dt><dt><span class="sect2"><a href="ch04.html#d0e7491">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
index 55b6420..6421664 100644 (file)
Binary files a/doc/kivitendo-Dokumentation.pdf and b/doc/kivitendo-Dokumentation.pdf differ
diff --git a/image/kivitendo_corona.png b/image/kivitendo_corona.png
new file mode 100644 (file)
index 0000000..d6f3d03
Binary files /dev/null and b/image/kivitendo_corona.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>
index 8be5676..9fc14b6 100644 (file)
@@ -151,6 +151,7 @@ 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]);
index 0939361..8946fbc 100644 (file)
@@ -107,21 +107,23 @@ namespace('kivi.ActionBar', function(k){
     }
   };
 
-  k.removeTooltip = function($e) {
+  k.removeTooltip = function(e) {
+    var $e = $(e);
     if ($e.hasClass('tooltipstered'))
       $e.tooltipster('destroy');
     $e.prop('title', '');
   };
 
-  k.setTooltip = function($e, tooltip) {
+  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 data = $e.data('action');
+  k.setDisabled = function(e, tooltip) {
+    var $e = $(e);
 
     $e.addClass(CLASSES.disabled);
 
@@ -131,7 +133,8 @@ namespace('kivi.ActionBar', function(k){
       kivi.ActionBar.removeTooltip($e);
   };
 
-  k.setEnabled = function($e) {
+  k.setEnabled = function(e) {
+    var $e   = $(e);
     var data = $e.data('action');
 
     $e.removeClass(CLASSES.disabled);
index f809e34..e8cd923 100644 (file)
@@ -58,7 +58,7 @@ 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;
@@ -67,10 +67,14 @@ namespace('kivi.CustomerVendor', function(ns) {
 
       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
@@ -79,7 +83,6 @@ namespace('kivi.CustomerVendor', function(ns) {
         params.onFormSet();
     });
 
-    $('#contact_cp_title_select, #contact_cp_abteilung_select').val('');
   };
 
   var mapSearchStmts = [
@@ -455,7 +458,39 @@ namespace('kivi.CustomerVendor', function(ns) {
     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();
   });
 });
index 833d928..374113c 100644 (file)
@@ -19,8 +19,9 @@ namespace('kivi.GL', function(ns) {
 
     $.ajax({
       url: 'gl.pl?action=get_tax_dropdown',
-      data: { accno_id:  $(obj).val(),
-              transdate: $('#transdate').val() },
+      data: { accno_id:     $(obj).val(),
+              transdate:    $('#transdate').val(),
+              deliverydate: $('#deliverydate').val() },
       dataType: 'html',
       success: function (new_html) {
         $("#taxchart_" + row).html(new_html);
index 93fa162..20ecde0 100644 (file)
@@ -104,6 +104,7 @@ namespace('kivi.MassInvoiceCreatePrint', function(ns) {
     $('#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();
index e7815d4..288af2d 100644 (file)
@@ -28,7 +28,7 @@ namespace('kivi.Order', function(ns) {
     }
 
     if (pos.length > 0) {
-      question = question || kivi.t8("Do you really want to save?");
+      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);
@@ -51,6 +51,7 @@ namespace('kivi.Order', function(ns) {
     if (warn_on_reqdate    && !ns.check_valid_reqdate())   return;
 
     var data = $('#order_form').serializeArray();
+    data.push({ name: 'order.language_id', value: $('#language_id').val() }); // language from print options
     data.push({ name: 'action', value: 'Order/' + action });
 
     $.post("controller.pl", data, kivi.eval_json_result);
@@ -82,29 +83,12 @@ namespace('kivi.Order', function(ns) {
 
     var data = $('#order_form').serializeArray();
     data = data.concat($('#print_options_form').serializeArray());
+    data.push({ name: 'order.language_id', value: $('#language_id').val() }); // language from print options
     data.push({ name: 'action', value: 'Order/print' });
 
     $.post("controller.pl", data, kivi.eval_json_result);
   };
 
-  ns.download_pdf = function(pdf_filename, key) {
-    var data = [{ name: 'action',       value: 'Order/download_pdf' },
-                { name: 'type',         value: $('#type').val()     },
-                { name: 'pdf_filename', value: pdf_filename         },
-                { name: 'key',          value: key                  }];
-    $.download("controller.pl", data);
-  };
-
-  ns.email = function(warn_on_duplicates) {
-    if (warn_on_duplicates && !ns.check_duplicate_parts(kivi.t8("Do you really want to send by mail?"))) return;
-    if (!ns.check_cv()) return;
-
-    var data = $('#order_form').serializeArray();
-    data.push({ name: 'action', value: 'Order/show_email_dialog' });
-
-    $.post("controller.pl", data, kivi.eval_json_result);
-  };
-
   var email_dialog;
 
   ns.setup_send_email_dialog = function() {
@@ -155,6 +139,7 @@ namespace('kivi.Order', function(ns) {
     var data = $('#order_form').serializeArray();
     data = data.concat($('[name^="email_form."]').serializeArray());
     data = data.concat($('[name^="print_options."]').serializeArray());
+    data.push({ name: 'order.language_id', value: $('#language_id').val() }); // language from print options
     data.push({ name: 'action', value: 'Order/send_email' });
     $.post("controller.pl", data, kivi.eval_json_result);
   };
@@ -167,7 +152,8 @@ namespace('kivi.Order', function(ns) {
     $('#nr_in_title').html($(elt).val());
   };
 
-  ns.reload_cv_dependant_selections = function() {
+  ns.reload_cv_dependent_selections = function() {
+    $('#order_shipto_id').val('');
     var data = $('#order_form').serializeArray();
     data.push({ name: 'action', value: 'Order/customer_vendor_changed' });
 
@@ -178,6 +164,68 @@ namespace('kivi.Order', function(ns) {
     $(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() {
     var data = $('#order_form').serializeArray();
     data.push({ name: 'action', value: 'Order/recalc_amounts_and_taxes' });
@@ -334,10 +382,19 @@ namespace('kivi.Order', function(ns) {
     });
   };
 
+  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) {
@@ -373,14 +430,33 @@ namespace('kivi.Order', function(ns) {
     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: 'Order/add_item' });
+    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);
   };
@@ -438,12 +514,12 @@ namespace('kivi.Order', function(ns) {
   ns.multi_items_dialog_disable_continue = function() {
     // disable keydown-event and continue button to prevent
     // impatient users to add parts multiple times
-    $('#multi_items_result input').off("keydown");
+    $('#multi_items_result input, #multi_items_position').off("keydown");
     $('#multi_items_dialog_continue_button').prop('disabled', true);
   };
 
   ns.multi_items_dialog_enable_continue = function()  {
-    $('#multi_items_result input').keydown(function(event) {
+    $('#multi_items_result input, #multi_items_position').keydown(function(event) {
       if(event.keyCode == 13) {
         event.preventDefault();
         ns.add_multi_items();
@@ -474,9 +550,12 @@ namespace('kivi.Order', function(ns) {
 
     ns.multi_items_dialog_disable_continue();
 
+    var insert_before_item_id = ns.get_insert_before_item_id($('#multi_items_position').val());
+
     var data = $('#order_form').serializeArray();
     data = data.concat($('#multi_items_form').serializeArray());
-    data.push({ name: 'action', value: 'Order/add_multi_items' });
+    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);
   };
 
@@ -719,6 +798,34 @@ namespace('kivi.Order', function(ns) {
     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');
@@ -728,15 +835,90 @@ namespace('kivi.Order', function(ns) {
     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 }});
+  };
+
 });
 
 $(function() {
   if ($('#type').val() == 'sales_order' || $('#type').val() == 'sales_quotation' ) {
-    $('#order_customer_id').change(kivi.Order.reload_cv_dependant_selections);
+    $('#order_customer_id').change(kivi.Order.reload_cv_dependent_selections);
   } else {
-    $('#order_vendor_id').change(kivi.Order.reload_cv_dependant_selections);
+    $('#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);
+
   if ($('#type').val() == 'sales_order' || $('#type').val() == 'sales_quotation' ) {
     $('#add_item_parts_id').on('set_item:PartPicker', function(e,o) { $('#add_item_sellprice_as_number').val(kivi.format_amount(o.sellprice, -2)) });
   } else {
@@ -783,4 +965,6 @@ $(function() {
     return false;
   });
 
+  $('.reformat_number_as_null_number').change(kivi.Order.reformat_number_as_null_number);
+
 });
index 0b0185b..4f813bb 100644 (file)
@@ -334,7 +334,6 @@ namespace('kivi.Part', function(ns) {
     },
     ajax_data: function(term) {
       var data = {
-        'filter.all:substr:multi::ilike': term,
         current:  this.$real.val(),
       };
 
@@ -356,6 +355,15 @@ namespace('kivi.Part', function(ns) {
       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) {
@@ -687,6 +695,11 @@ namespace('kivi.Part', function(ns) {
       var self = this;
       var data = $('#multi_items_form').serializeArray();
       data.push({ name: 'type', value: self.pp.type });
+      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,
index 86704f2..2b63a1a 100644 (file)
@@ -170,8 +170,9 @@ namespace('kivi.SalesPurchase', function(ns) {
     $('#shiptoname').focus();
   };
 
-  this.submit_custom_shipto = function() {
-    $('#shipto_id').val('');
+  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');
   };
@@ -268,21 +269,23 @@ namespace('kivi.SalesPurchase', function(ns) {
 
     var vc   = $('#vc').val();
     var data = {
-      action:      'show_sales_purchase_email_dialog',
-      cp_id:       $('#cp_id').val(),
-      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(),
-      rowcount:    $('#rowcount').val(),
-      quonumber:   $('#quonumber').val(),
-      type:        $('#type').val(),
-      vc:          vc,
-      vc_id:       $('#' + vc + '_id').val(),
+      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').val(),
     };
 
     $('[name^=id_],[name^=partnumber_]').each(function(idx, elt) {
index 4448921..a76ad8a 100644 (file)
@@ -558,6 +558,39 @@ namespace("kivi", function(ns) {
     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;
index daad4ca..2423cf2 100644 (file)
@@ -16,6 +16,7 @@ namespace("kivi").setupLocale({
 "Assign invoice":"Rechnung zuweisen",
 "Basic settings actions":"Aktionen zu Grundeinstellungen",
 "Cancel":"Abbrechen",
+"Carry over shipping address":"Lieferadresse übernehmen",
 "Chart picker":"Kontenauswahl",
 "Copy":"Kopieren",
 "Copy requirement spec":"Pflichtenheft kopieren",
@@ -38,16 +39,15 @@ namespace("kivi").setupLocale({
 "Delete requirement spec":"Pflichtenheft löschen",
 "Delete template":"Vorlage löschen",
 "Delete text block":"Textblock löschen",
-"Do you really want do continue?":"Möchten Sie wirklich fortfahren?",
 "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 save?":"Möchten Sie wirklich speichern?",
-"Do you really want to send by mail?":"Wollen Sie den Beleg wirklich per Mail verschicken?",
 "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!",
@@ -143,6 +143,8 @@ namespace("kivi").setupLocale({
 "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:",
 "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",
index 87bc1b7..7027a28 100644 (file)
@@ -16,6 +16,7 @@ namespace("kivi").setupLocale({
 "Assign invoice":"",
 "Basic settings actions":"",
 "Cancel":"",
+"Carry over shipping address":"",
 "Chart picker":"",
 "Copy":"",
 "Copy requirement spec":"",
@@ -38,16 +39,15 @@ namespace("kivi").setupLocale({
 "Delete requirement spec":"",
 "Delete template":"",
 "Delete text block":"",
-"Do you really want do continue?":"",
 "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 save?":"",
-"Do you really want to send by mail?":"",
 "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!":"",
@@ -143,6 +143,8 @@ namespace("kivi").setupLocale({
 "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:":"",
 "filename has not uploadable characters ":"",
 "filesize too big: ":"",
 "flat-rate position":"",
index 59c6746..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 = {};
 
index 771d5c8..bec30d1 100755 (executable)
@@ -14,7 +14,6 @@ $self->{texts} = {
   ' bytes, max='                => ' Bytes, Maximum=',
   ' missing!'                   => ' fehlt!',
   '#1 (custom variable)'        => '#1 (benutzerdefinierte Variable)',
-  '#1 CB transactions and #1 OB transactions generated.' => '#1 Schluss- und #1 Eröffnungsbuchungen wurden erstellt.',
   '#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',
@@ -80,7 +79,7 @@ $self->{texts} = {
   'AP transaction posted.'      => 'Kreditorenbuchung verbucht.',
   '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/Verbindunglichkeiten & Buchungsjournal',
+  'AP/AR Aging & Journal'       => 'Offene Forderungen/Verbindlichkeiten & Buchungsjournal',
   'AR'                          => 'Verkauf',
   'AR Aging'                    => 'Offene Forderungen',
   'AR Transaction'              => 'Debitorenbuchung',
@@ -137,6 +136,7 @@ $self->{texts} = {
   'Account deleted!'            => 'Konto gelöscht!',
   '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',
@@ -161,6 +161,7 @@ $self->{texts} = {
   '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',
@@ -204,6 +205,7 @@ $self->{texts} = {
   'Add department'              => 'Abteilung hinzufügen',
   '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',
@@ -216,6 +218,7 @@ $self->{texts} = {
   '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',
@@ -233,6 +236,7 @@ $self->{texts} = {
   'Add sub function block'      => 'Unterfunktionsblock hinzufügen',
   'Add taxzone'                 => 'Steuerzone hinzufügen',
   'Add text block'              => 'Textblock 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',
@@ -245,6 +249,7 @@ $self->{texts} = {
   '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',
@@ -274,7 +279,7 @@ $self->{texts} = {
   'Allow to delete generated printfiles' => 'Löschen von erzeugten Dokumenten erlaubt',
   'Already counted'             => 'Bereits erfasst',
   '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 save orders with a projectnumber (create new projects)' => 'Aufträge immer mit Projektnummer speichern (neue Projekt erstellen)',
+  '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',
   'Amount'                      => 'Betrag',
   'Amount (for verification)'   => 'Betrag (zur Überprüfung)',
@@ -286,8 +291,8 @@ $self->{texts} = {
   '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 occured. Letter could not be deleted.' => 'Es ist ein Fehler aufgetreten. Der Brief konnte nicht gelöscht werden.',
   '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).',
@@ -296,7 +301,6 @@ $self->{texts} = {
   '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 an %s',
-  'Application Error. No Format given' => 'Fehler in der Anwendung. Das Ausgabeformat fehlt.',
   'Application Error. Wrong Format' => 'Fehler in der Anwendung. Falsches Format: ',
   'Apply'                       => 'Anwenden',
   'Apply customer'              => 'Kunde hinzufügen',
@@ -306,12 +310,14 @@ $self->{texts} = {
   '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 generate cb/ob transactions?' => 'Sollen die EB/SB Buchungen wirklich erzeugt werden?',
+  '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 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?',
@@ -348,6 +354,7 @@ $self->{texts} = {
   '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',
@@ -355,7 +362,6 @@ $self->{texts} = {
   'Attachment name'             => 'Name des Anhangs',
   'Attachments'                 => 'Dateianhänge',
   'Attempt to call an undefined sub named \'%s\'' => 'Es wurde versucht, eine nicht definierte Unterfunktion namens \'%s\' aufzurufen.',
-  'Attention: Here will be generated a lot of CB/OB transactions.' => 'Hiermit werden Buchungen für den Schlussbestand (SB-Buchung) und den Eröffnungsbestand (EB-Buchung) für mehrere Konten gleichzeitig gebucht.<br>In diesem Schritt wird das Datum der Buchungen sowie das Saldovortragskonto festgelegt.<br>Das Datum der SB-Buchung wird außerdem verwendet um das Saldo der Konten zu ermitteln, welche im nächsten Schritt (nach "Weiter") angezeigt werden.',
   'Audit Control'               => 'Bücherkontrolle',
   'Aug'                         => 'Aug',
   'August'                      => 'August',
@@ -388,8 +394,10 @@ $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',
@@ -473,6 +481,7 @@ $self->{texts} = {
   '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',
@@ -494,8 +503,6 @@ $self->{texts} = {
   'CANCELED'                    => 'Storniert',
   'CB Transaction'              => 'SB-Buchung',
   'CB Transactions'             => 'SB-Buchungen',
-  'CB date #1 is higher than OB date #2. Please select again.' => 'SB-Datum #1 ist größer als EB-Datum #2. Bitte sinnvollere Werte auswählen.',
-  'CB/OB Transactions'          => 'SB/EB buchen',
   'CN'                          => 'Kd-Nr.',
   'CR'                          => 'H',
   'CSS style for pictures'      => 'CSS Style für Bilder',
@@ -507,6 +514,7 @@ $self->{texts} = {
   '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',
@@ -524,6 +532,7 @@ $self->{texts} = {
   '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 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 delete account!'      => 'Konto kann nicht gelöscht werden!',
   'Cannot delete customer!'     => 'Kunde kann nicht gelöscht werden!',
@@ -564,10 +573,12 @@ $self->{texts} = {
   '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',
   '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',
@@ -638,6 +649,7 @@ $self->{texts} = {
   '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',
@@ -646,6 +658,7 @@ $self->{texts} = {
   'Company'                     => 'Firma',
   'Company Name'                => 'Firmenname',
   'Company name'                => 'Firmenname',
+  'Company name and address'    => 'Firmenname und -adresse',
   'Company settings'            => 'Firmeneinstellungen',
   'Compare to'                  => 'Gegenüberstellen zu',
   'Complexities'                => 'Komplexitätsgrade',
@@ -656,9 +669,11 @@ $self->{texts} = {
   '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)',
@@ -672,7 +687,7 @@ $self->{texts} = {
   '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:'                 => 'Konversiom',
+  'Conversion:'                 => 'Konversion',
   'Converting to deliveryorder' => 'Konvertiere zu Lieferschein',
   'Copies'                      => 'Kopien',
   'Copy'                        => 'Kopieren',
@@ -684,6 +699,8 @@ $self->{texts} = {
   'Correct taxkey'              => 'Richtiger Steuerschlüssel',
   'Cost Center'                 => 'Kostenstelle',
   'Costs'                       => 'Kosten',
+  'Could not create new project #1' => 'Neues Projekt #1 kann nicht angelegt werden',
+  'Could not extract ZUGFeRD data, data and error message:' => 'Konnte keine ZUGFeRD Daten extrahieren, folgende Fehlermeldung und das PDF:',
   'Could not find an entry for this part in the pricegroup.' => 'Konnte keine 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"',
@@ -704,6 +721,8 @@ $self->{texts} = {
   'Create Date'                 => 'Erstelldatum',
   'Create HTML'                 => 'HTML erzeugen',
   'Create PDF'                  => 'PDF erzeugen',
+  'Create ZUGFeRD invoices'     => 'ZUGFeRD-Rechnungen erzeugen',
+  'Create ZUGFeRD invoices in test mode' => 'ZUGFeRD-Rechnungen im Testmodus erzeugen',
   'Create a new background job' => 'Einen neuen Hintergrund-Job anlegen',
   'Create a new client'         => 'Einen neuen Mandanten anlegen',
   'Create a new delivery term'  => 'Neue Lieferbedingungen anlegen',
@@ -737,7 +756,7 @@ $self->{texts} = {
   '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 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',
@@ -757,6 +776,7 @@ $self->{texts} = {
   '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 ZUGFeRD data' => 'Verkaufsrechnungen mit ZUGFeRD-Daten erzeugen',
   'Create tables'               => 'Tabellen anlegen',
   'Created by'                  => 'Erstellt von',
   'Created for'                 => 'Erstellt für',
@@ -856,7 +876,7 @@ $self->{texts} = {
   'DATEV Export'                => 'DATEV-Export',
   '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 westeuropäische Zeichenkodierung (LATIN-1, cp1252). Die Einstellung "Strikt und Abbruch" erlaubt keine nicht kodiebaren 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-Expport 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ätze enthalten und armenische Zeiche wie "Գեղարդ" könnnen sicherlich überhaupt nicht mit Zeichen in cp1252 ersetzt werden.',
+  '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',
@@ -939,6 +959,7 @@ $self->{texts} = {
   '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',
@@ -984,6 +1005,7 @@ $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)',
@@ -1025,6 +1047,7 @@ $self->{texts} = {
   '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 ZUGFeRD invoices' => 'Keine ZUGFeRD-Rechnungen erzeugen',
   'Do not link to a project.'   => 'Nicht mit einem Projekt verknüpfen.',
   'Do not modify this position' => 'Diese Position nicht verändern',
   'Do not run the task server for this client' => 'Task-Server nicht für diesen Mandanten ausführen',
@@ -1032,12 +1055,12 @@ $self->{texts} = {
   '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?' => 'Möchten Sie wirklich fortfahren?',
   '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?',
@@ -1049,8 +1072,6 @@ $self->{texts} = {
   '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 save?' => 'Möchten Sie wirklich speichern?',
-  'Do you really want to send by mail?' => 'Wollen Sie den Beleg wirklich per Mail verschicken?',
   '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>?',
@@ -1171,6 +1192,7 @@ $self->{texts} = {
   'Edit Vendor'                 => 'Lieferant editieren',
   'Edit Vendor Invoice'         => 'Einkaufsrechnung bearbeiten',
   'Edit Warehouse'              => 'Lager bearbeiten',
+  'Edit ZUGFeRD notes'          => 'ZUGFeRD-Notizen bearbeiten',
   'Edit acceptance status'      => 'Abnahmestatus bearbeiten',
   'Edit additional articles'    => 'Zusätzliche Artikel bearbeiten',
   'Edit all drafts'             => 'Entwürfe von allen Benutzern bearbeiten',
@@ -1188,6 +1210,7 @@ $self->{texts} = {
   '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',
@@ -1227,6 +1250,7 @@ $self->{texts} = {
   'Edit the request_quotation'  => 'Bearbeiten der Preisanfrage',
   'Edit the sales_order'        => 'Bearbeiten des Auftrags',
   'Edit the sales_quotation'    => 'Bearbeiten des Angebots',
+  'Edit title'                  => 'Titiel bearbeiten',
   'Edit units'                  => 'Einheiten bearbeiten',
   'Edit user signature'         => 'Benutzersignatur bearbeiten',
   'Editable'                    => 'Bearbeitbar',
@@ -1254,6 +1278,7 @@ $self->{texts} = {
   'Equity'                      => 'Passiva',
   'Erfolgsrechnung'             => 'Erfolgsrechnung',
   'Error'                       => 'Fehler',
+  '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ü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.',
@@ -1262,6 +1287,8 @@ $self->{texts} = {
   '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 with default taxzone'  => 'Ungültige Standardsteuerzone',
   'Error!'                      => 'Fehler!',
   'Error: #1'                   => 'Fehler: #1',
@@ -1273,6 +1300,7 @@ $self->{texts} = {
   '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',
@@ -1299,11 +1327,17 @@ $self->{texts} = {
   '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',
@@ -1332,8 +1366,10 @@ $self->{texts} = {
   '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:',
   'Ertrag'                      => 'Ertrag',
   'Ertrag prozentual'           => 'Ertrag prozentual',
   'Escape character'            => 'Escape-Zeichen',
@@ -1499,7 +1535,6 @@ $self->{texts} = {
   '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',
   'Generate and print sales delivery orders' => 'Erzeuge und drucke Lieferscheine',
-  'Generic Tax Report'          => 'USTVA Bericht',
   'Germany'                     => 'Deutschland',
   'Get shoporders'              => 'Shopbestellungen holen und bearbeiten',
   'Git revision: #1, #2 #3'     => 'Git-Revision: #1, #2 #3',
@@ -1576,6 +1611,7 @@ $self->{texts} = {
   '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 enabled ZUGFeRD-conformant sales invoice PDFs will be created.' => 'Falls aktiviert, werden 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 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.',
@@ -1584,12 +1620,14 @@ $self->{texts} = {
   '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 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 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 dieser Schwellenwert von der Menge in der Datenbank abweicht, wird eine Warnmeldung angezeigt. Setzen Sie den Schwellenwert auf 0, um dieses Feature abzuschalten.',
+  '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 the test mode is enabled, the ZUGFeRD invoices will be flagged so that they\'re only fit to be used for testing purposes.' => 'Wenn der Testmodus aktiviert ist, werden 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.',
@@ -1600,6 +1638,7 @@ $self->{texts} = {
   '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',
   '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',
@@ -1611,6 +1650,7 @@ $self->{texts} = {
   'Import CSV'                  => 'CSV-Import',
   'Import Status'               => 'Import Status',
   'Import a MT940 file:'        => 'Laden Sie eine MT940 Datei hoch:',
+  'Import a ZUGFeRD file:'      => 'Eine ZUGFeRD-Datei importieren',
   'Import all'                  => 'Importiere Alle',
   'Import documents from #1'    => 'Importiere Dateien von Quelle \'#1\'',
   'Import file'                 => 'Import-Datei',
@@ -1636,6 +1676,7 @@ $self->{texts} = {
   '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',
@@ -1667,6 +1708,7 @@ $self->{texts} = {
   'Introduction of clients'     => 'Einführung von Mandanten',
   'Inv. Duedate'                => 'Rg. Fälligkeit',
   'Invalid'                     => 'Ungültig',
+  '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ültige Wiedervorlage-ID.',
@@ -1711,6 +1753,7 @@ $self->{texts} = {
   '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 sales'                    => 'Verkauf',
   '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!',
@@ -1801,13 +1844,14 @@ $self->{texts} = {
   'Link to the following project:' => 'Mit dem folgenden Projekt verknüpfen:',
   'Linked Records'              => 'Verknüpfte Belege',
   'Linked invoices'             => 'Verknüpfte Rechnungen',
-  'Linked positions will always be reconciled first. If this is set to yes, unlinked positions will be reconciled in a second step. This is necessary in very old databases (with open delivery orders from before 3.4.0) and in businesses where delivery orders are frequently amended. Usually the direct links are faster and more accurate. Defaults to true for historical reasons only.' => 'Verlinkte Positionen werden immer zuerst abgeglichen. Wenn diese Option aktiviert ist, werden danach nicht verlinkte Lieferscheinpositionen mit den restlichen nicht vollständig gelieferten Auftragspositionen abgeglichen. Notwendig in alten Datenbanken (mit offenen Lieferscheinen von vor 3.4.0) und in Betrieben in denen Lieferscheine nachträglich ergänzt werden. In allen anderen Fällen ist es schneller und korrekter diese Methode zu deaktivieren. Die Voreinstellung auf "Ja" is aus Kompatibilitätsgründen.',
+  'Linked positions will always be reconciled first. If this is set to yes, unlinked positions will be reconciled in a second step. This is necessary in very old databases (with open delivery orders from before 3.4.0) and in businesses where delivery orders are frequently amended. Usually the direct links are faster and more accurate. Defaults to true for historical reasons only.' => 'Verlinkte Positionen werden immer zuerst abgeglichen. Wenn diese Option aktiviert ist, werden danach nicht verlinkte Lieferscheinpositionen mit den restlichen nicht vollständig gelieferten Auftragspositionen abgeglichen. Notwendig in alten Datenbanken (mit offenen Lieferscheinen von vor 3.4.0) und in Betrieben in denen Lieferscheine nachträglich ergänzt werden. In allen anderen Fällen ist es schneller und korrekter diese Methode zu deaktivieren. Seit 3.5.6 standardmäßig deaktiviert.',
   'Liquidity projection'        => 'Liquiditätsübersicht',
   'List Accounts'               => 'Konten 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',
@@ -1828,6 +1872,7 @@ $self->{texts} = {
   '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',
@@ -1845,6 +1890,8 @@ $self->{texts} = {
   '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',
+  'Loss'                        => 'Verlust',
+  'Loss carried forward account' => 'Verlustvortragskonto',
   'Luxembourg'                  => 'Luxemburg',
   'MAILED'                      => 'Gesendet',
   'MD'                          => 'PT',
@@ -1880,6 +1927,7 @@ $self->{texts} = {
   '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',
@@ -1906,6 +1954,7 @@ $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',
@@ -1937,6 +1986,7 @@ $self->{texts} = {
   '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 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',
@@ -1971,6 +2021,7 @@ $self->{texts} = {
   'Next run at'                 => 'Nächste Ausführung um',
   'No'                          => 'Nein',
   '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 Company Address given'    => 'Keine Firmenadresse hinterlegt!',
   'No Company Name given'       => 'Kein Firmenname hinterlegt!',
@@ -1979,6 +2030,7 @@ $self->{texts} = {
   'No Journal'                  => 'Kein Journal',
   'No Shopdescription'          => 'Keine Shop-Artikelbeschreibung',
   'No Shopimages'               => 'Keine Shop-Bilder',
+  'No VAT Info for this ZUGFeRD invoice, please ask your vendor to add this for his ZUGFeRD data.' => 'Konnte keine UST-ID für diese ZUGFeRD Rechnungen finden, bitte fragen Sie bei Ihren Lieferanten nach, ob dieses Feld im ZUGFeRD Datensatz gesetzt wird.',
   'No Vendor was found matching the search parameters.' => 'Zu dem Suchbegriff wurde kein Händler gefunden',
   'No action defined.'          => 'Keine Aktion definiert.',
   'No article has been selected yet.' => 'Es wurde noch kein Artikel ausgewählt.',
@@ -1986,9 +2038,11 @@ $self->{texts} = {
   '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 flagged for ZUGFeRD usage was found.' => 'Es wurde kein Bankkonto gefunden, das für Nutzung mit ZUGFeRD markiert ist.',
   '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ä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 contact selected to delete' => 'Keine Ansprechperson zum Löschen ausgewählt',
@@ -2017,7 +2071,6 @@ $self->{texts} = {
   '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 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',
@@ -2026,11 +2079,12 @@ $self->{texts} = {
   '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 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 templates have been created yet.' => 'Es wurden noch keine Pflichtenheftvorlagen angelegt.',
   'No results.'                 => 'Keine Artikel',
-  'No revert available.'        => 'Dieser Vorgang kann nicht rückgängig gemacht werden, ggf. falsch erstellte Buchungen müssen einzeln manuell korrigiert werden.',
   '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.',
@@ -2067,6 +2121,7 @@ $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',
   'Note'                        => 'Hinweis',
   'Note that parameter names must not be quoted.' => 'Beachten Sie, dass Parameternamen nicht in Anführungszeichen stehen dürfen.',
@@ -2106,7 +2161,6 @@ $self->{texts} = {
   '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',
-  'OB Transactions'             => 'EB-Buchungen',
   'Objects have been imported.' => 'Objekte wurden importiert.',
   'Obsolete'                    => 'Ungültig',
   'Oct'                         => 'Okt',
@@ -2119,14 +2173,13 @@ $self->{texts} = {
   '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 OB-transaction'          => 'Eine EB-Buchung',
-  'One SB-transaction'          => 'Eine SB-Buchung',
   '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 Lines with Notes or Errors' => 'Nur Zeilen mit Bemerkungen oder Fehlern',
   'Only Price'                  => 'Nur Preis',
   'Only Stock'                  => 'Nur Bestand',
-  'Only Warnings and Errors'    => 'Nur Warnungen und Fehler',
   'Only applies if the previous is set to true. When filling up unlinked positions, consider them matches if ALL of these fields match. For example, in a business with variants that are defined by a special description, description needs to be part of the identity. If delivering several similar order positions by delivery date is common, reqdate should be included in the identity. Serialnumber is useful when the serialnumber in the order and delivery order has to match.' => 'Ist nur relevant, wenn die vorherige Option angeschaltet ist. Zugewiesene Zeilen müssen in diesen Feldern identisch sein, und werden ansonsten als unterschiedlich behandelt. Wenn ein Betrieb mit Varianten arbeitet, die in der Beschreibung kodiert sind, muss diese mit abgeglichen werden. Wenn Positionen mit Lieferdaten versehen werden, sollten diese mit abgeglichen werden. Seriennummer abzugleichen funktioniert nur, wenn diese in Auftrag und Lieferschein gepflegt werden.',
   'Only booked accounts'        => 'Nur bebuchte Konten',
   'Only due follow-ups'         => 'Nur fällige Wiedervorlagen',
@@ -2164,6 +2217,7 @@ $self->{texts} = {
   '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',
@@ -2210,6 +2264,7 @@ $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 automatically.' => 'Bei Artikel "#1" ist eine Chargenummer oder ein Mindesthaltbarkeitsdatum vergeben. Deshalb kann dieser Artikel nicht automatisch ausgelagert werden.',
   'Part (database ID)'          => 'Artikel (Datenbank-ID)',
@@ -2251,6 +2306,7 @@ $self->{texts} = {
   '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 ',
@@ -2292,11 +2348,13 @@ $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 add a valid VAT-ID for this vendor: ' => 'Bitte prüfen Sie ob dieser Lieferant eine valide UST-ID (Großschreibungen und Leerzeichen beachten) besitzt:',
   '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.',
@@ -2376,6 +2434,7 @@ $self->{texts} = {
   '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',
   'Preview'                     => 'Vorschau',
@@ -2386,6 +2445,7 @@ $self->{texts} = {
   'Price #1'                    => 'Preis #1',
   'Price Factor'                => 'Preisfaktor',
   'Price Factors'               => 'Preisfaktoren',
+  'Price List'                  => 'Preisliste',
   'Price Rule'                  => 'Preisregel',
   'Price Rules'                 => 'Preisregeln',
   'Price Source'                => 'Preisquelle',
@@ -2441,6 +2501,9 @@ $self->{texts} = {
   'Production'                  => 'Produktion',
   'Production (typeabbreviation)' => 'P',
   'Productivity'                => 'Produktivität',
+  'Profit'                      => 'Gewinn',
+  'Profit and loss accounts'    => 'Erfolgskonten',
+  'Profit carried forward account' => 'Gewinnvortragskonto',
   'Profit determination'        => 'Gewinnermittlung',
   'Proforma Invoice'            => 'Proformarechnung',
   'Program'                     => 'Programm',
@@ -2685,7 +2748,6 @@ $self->{texts} = {
   'SEPA strings'                => 'SEPA-Überweisungen',
   'SQL query'                   => 'SQL-Abfrage',
   'SWIFT MT940 format'          => 'SWIFT-MT940-Format',
-  'Saldo'                       => 'Saldo',
   'Saldo Credit'                => 'Saldo Haben',
   'Saldo Debit'                 => 'Saldo Soll',
   'Saldo neu'                   => 'Saldo neu',
@@ -2774,6 +2836,9 @@ $self->{texts} = {
   'Search AR Aging'             => 'Offene Forderungen',
   'Search bank transactions'    => 'Filter für Bankbuchungen',
   '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',
@@ -2786,7 +2851,6 @@ $self->{texts} = {
   'Select Mulit-Item Options'   => 'Multi-Treffer Auswahlliste',
   'Select a Customer'           => 'Endkunde auswählen',
   'Select a period'             => 'Bitte Zeitraum auswählen',
-  'Select charts for which the CB/OB transactions want to be posted.' => 'Wählen Sie Konten aus, zu welchen SB/EB-Buchungen erstellt werden sollen.',
   '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',
@@ -2949,6 +3013,7 @@ $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',
@@ -2992,6 +3057,7 @@ $self->{texts} = {
   'Start of year'               => 'Jahresanfang',
   'Start process'               => 'Prozess starten',
   'Start the correction assistant' => 'Korrekturassistenten starten',
+  'Startdate method'            => 'Methode zur Ermittlung des Startdatums',
   'Startdate_coa'               => 'Gültig ab',
   'Starting Balance'            => 'Eröffnungsbilanzwerte',
   'Starting balance'            => 'Anfangssaldo',
@@ -3014,7 +3080,9 @@ $self->{texts} = {
   '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 value'                 => 'Bestandswert',
+  'StockInfo'                   => 'Lagerinfo',
   'Stocked Qty'                 => 'Lagermenge',
   'Stocktaking'                 => 'Inventur',
   'Stocktaking History'         => 'Inventur Historie',
@@ -3033,6 +3101,8 @@ $self->{texts} = {
   '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',
@@ -3045,10 +3115,8 @@ $self->{texts} = {
   'Subtotals per quarter'       => 'Zwischensummen pro Quartal',
   'Such entries cannot be exported into the DATEV format and have to be fixed as well.' => 'Solche Einträge sind aber nicht DATEV-exportiertbar und müssen ebenfalls korrigiert werden.',
   'Suggested invoice'           => 'Rechnungsvorschlag',
-  'Sum CB Transactions'         => 'Summe SB',
   'Sum Credit'                  => 'Summe Haben',
   'Sum Debit'                   => 'Summe Soll',
-  'Sum OB Transactions'         => 'Summe EB',
   'Sum for'                     => 'Summe für',
   'Sum for #1'                  => 'Summe für #1',
   'Sum for section'             => 'Summe für Abschnitt',
@@ -3088,7 +3156,6 @@ $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',
@@ -3113,7 +3180,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',
@@ -3164,7 +3230,15 @@ $self->{texts} = {
   '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 XMP metadata does not declare the ZUGFeRD data.' => 'Die XMP-Metadaten enthalten keine ZUGFeRD-Deklaration.',
+  'The ZUGFeRD XML invoice was not found.' => 'Die ZUGFeRD-XML-Rechnungsdaten wurden nicht gefunden.',
+  '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 ZUGFeRD notes have been saved.' => 'Die ZUGFeRD-Notizen wurden gespeichert.',
+  'The ZUGFeRD version used is not supported.' => 'Die verwendete ZUGFeRD-Version wird nicht unterstützt.',
   '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.',
@@ -3217,6 +3291,7 @@ $self->{texts} = {
   '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 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.',
@@ -3226,8 +3301,11 @@ $self->{texts} = {
   '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.',
@@ -3235,6 +3313,7 @@ $self->{texts} = {
   '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\'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.',
@@ -3274,6 +3353,8 @@ $self->{texts} = {
   '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',
@@ -3507,7 +3588,6 @@ $self->{texts} = {
   '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 will be two transactions done:' => 'Zu jedem ausgewählten Konto werden jeweils zwei Buchungen erstellt:',
   '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.',
@@ -3515,6 +3595,8 @@ $self->{texts} = {
   '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.',
@@ -3591,7 +3673,6 @@ $self->{texts} = {
   'Time estimate'               => 'Zeitschätzung',
   'Time period for the analysis:' => 'Analysezeitraum:',
   'Time/cost estimate actions'  => 'Aktionen für Kosten-/Zeitabschätzung',
-  'Timerange'                   => 'Zeitraum',
   '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',
@@ -3690,6 +3771,7 @@ $self->{texts} = {
   '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',
@@ -3705,6 +3787,7 @@ $self->{texts} = {
   '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 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',
@@ -3721,6 +3804,7 @@ $self->{texts} = {
   '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',
@@ -3761,14 +3845,22 @@ $self->{texts} = {
   '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 default booking group because setting is \'all\'' => 'Standardbuchungsgruppe wird verwendet',
   'Use default booking group because wanted is missing' => 'Fehlende Buchungsgruppe, deshalb Standardbuchungsgruppe',
   'Use default warehouse for assembly transfer' => 'Zum Fertigen Standardlager des Bestandteils verwenden',
   'Use existing templates'      => 'Vorhandene Druckvorlagen verwenden',
   'Use fill up when calculating shipped quantities?' => 'Sollen nicht verlinkte Positionen abgeglichen werden?',
+  'Use for ZUGFeRD'             => 'Nutzung mit ZUGFeRD',
   'Use linked items'            => 'Verknüpfte Positionen verwenden',
   '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',
@@ -3832,6 +3924,7 @@ $self->{texts} = {
   'View background job execution result' => 'Verlauf der Hintergrund-Job-Ausführungen anzeigen',
   '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',
@@ -3851,6 +3944,7 @@ $self->{texts} = {
   'Warn before saving orders without a delivery date' => 'Warnung ausgeben, falls Aufträge kein Lieferdatum 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',
@@ -3896,6 +3990,10 @@ $self->{texts} = {
   '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',
@@ -3918,10 +4016,12 @@ $self->{texts} = {
   '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 don\'t have the rights to edit this customer.' => 'Sie verfügen nicht über die erforderlichen Rechte, um diesen Kunden 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.',
@@ -3945,6 +4045,7 @@ $self->{texts} = {
   '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ä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',
@@ -3952,11 +4053,16 @@ $self->{texts} = {
   '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',
   'Your target quantity will be added to the stocked quantity.' => 'Ihre gezählte Zielmenge wird zum Lagerbestand hinzugezählt.',
+  'ZUGFeRD import'              => 'ZUGFeRD Import',
+  'ZUGFeRD invoice'             => 'ZUGFeRD-Rechnung',
+  'ZUGFeRD notes for each invoice' => 'ZUGFeRD-Notizen für jede Rechnung',
   'Zeitraum'                    => 'Zeitraum',
   'Zero amount posting!'        => 'Buchung ohne Wert',
   'Zip'                         => 'PLZ',
   'Zip, City'                   => 'PLZ, Ort',
   'Zipcode'                     => 'PLZ',
+  'Zipcode and city'            => 'PLZ und Stadt',
+  'ZugFeRD Import'              => 'ZUGFeRD Import',
   '[email]'                     => '[email]',
   'absolute'                    => 'absolut',
   'account_description'         => 'Beschreibung',
@@ -3984,19 +4090,20 @@ $self->{texts} = {
   '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 only parse a pdf file'   => 'Kann nur eine gültige PDF-Datei verwenden.',
   'cash'                        => 'Ist-Versteuerung',
   'chargenumber #1'             => 'Chargennummer #1',
   'chart_of_accounts'           => 'kontenuebersicht',
   'cleared'                     => 'Abgeglichen',
   'click here to edit cvars'    => 'Klicken Sie hier, um nach benutzerdefinierten Variablen zu suchen',
   'close'                       => 'schließen',
-  'close chart'                 => 'Saldovortragskonto',
   'closed'                      => 'geschlossen',
   'companylogo_subtitle'        => 'Lizenziert für',
   'config/kivitendo.conf: Key "DB_config" is missing.' => 'config/kivitendo.conf: Das Schlüsselwort "DB_config" fehlt.',
@@ -4033,6 +4140,8 @@ $self->{texts} = {
   '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',
@@ -4056,7 +4165,6 @@ $self->{texts} = {
   'from \'#1\' imported Files'  => 'Von \'#1\' importierte Dateien',
   'from (time)'                 => 'von',
   'general_ledger_list'         => 'Buchungsjournal',
-  'generate cb/ob transactions for selected charts' => 'Start-/Endbuchungen für ausgewählte Konten erstellen',
   'generated Files'             => 'Erzeugte Dokumente',
   'gobd-#1-#2.zip'              => 'gobd-#1-#2.zip',
   'h'                           => 'h',
@@ -4096,10 +4204,12 @@ $self->{texts} = {
   'male'                        => 'männlich',
   'max filesize'                => 'maximale Dateigröße',
   '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',
index 70e8d2b..e9e7e1e 100644 (file)
@@ -27,14 +27,8 @@ 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>=
 "=''
@@ -78,6 +72,7 @@ _=\\_
 Δ=$\\Delta$
 Ω=$\\Omega$
 ~={\\raisebox{0.5ex}{\\texttildelow}}
+‑={}-{}
 
 [Template/OpenDocument]
 order=& < > " ' \x80 \n \r
index acabd24..ea12150 100644 (file)
@@ -14,7 +14,6 @@ $self->{texts} = {
   ' bytes, max='                => '',
   ' missing!'                   => '',
   '#1 (custom variable)'        => '',
-  '#1 CB transactions and #1 OB transactions generated.' => '',
   '#1 MD'                       => '',
   '#1 additional part(s)'       => '',
   '#1 bank transaction bookings undone.' => '',
@@ -137,6 +136,7 @@ $self->{texts} = {
   'Account deleted!'            => '',
   'Account for fees'            => '',
   'Account for interest'        => '',
+  'Account for workflow from purchase order to ap transaction' => '',
   'Account number'              => '',
   'Account number not unique!'  => '',
   'Account number of the goal/source' => '',
@@ -161,6 +161,7 @@ $self->{texts} = {
   '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'           => '',
@@ -204,6 +205,7 @@ $self->{texts} = {
   'Add department'              => '',
   'Add empty line (csv_import)' => '',
   'Add function block'          => '',
+  'Add greeting'                => '',
   'Add headers from last uploaded file (csv_import)' => '',
   'Add invoices'                => '',
   'Add language'                => '',
@@ -216,6 +218,7 @@ $self->{texts} = {
   'Add new price rule item'     => '',
   'Add new record template'     => '',
   'Add note'                    => '',
+  'Add open Credit Notes'       => '',
   'Add part'                    => '',
   'Add part classification'     => '',
   'Add partsgroup'              => '',
@@ -233,6 +236,7 @@ $self->{texts} = {
   'Add sub function block'      => '',
   'Add taxzone'                 => '',
   'Add text block'              => '',
+  'Add title'                   => '',
   'Add unit'                    => '',
   'Added sections and function blocks: #1' => '',
   'Added text blocks: #1'       => '',
@@ -245,6 +249,7 @@ $self->{texts} = {
   'Administration'              => '',
   'Administration area'         => '',
   'Advance turnover tax return' => '',
+  'Advance turnover tax return only valid for SKR03 or SKR04' => '',
   'After closed period'         => '',
   'Aktion'                      => '',
   'All'                         => '',
@@ -286,8 +291,8 @@ $self->{texts} = {
   'Amount payable'              => '',
   'Amount payable less discount' => '',
   'Amounts differ too much'     => '',
-  'An error occured. Letter could not be deleted.' => '',
   '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).' => '',
@@ -296,7 +301,6 @@ $self->{texts} = {
   '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'              => '',
@@ -306,12 +310,14 @@ $self->{texts} = {
   'Apply to transfers without bin' => '',
   'Apply to transfers without comment' => '',
   'Apply to transfers without warehouse' => '',
+  'Apply year-end bookings'     => '',
   'Applying #1:'                => '',
   'Approximately #1 prices will be updated.' => '',
   'Apr'                         => '',
   'April'                       => '',
   'Ar aging on %s'              => '',
-  'Are you sure to generate cb/ob transactions?' => '',
+  '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 this letter?' => '',
   'Are you sure you want to remove the marked entries from the queue?' => '',
@@ -348,6 +354,7 @@ $self->{texts} = {
   '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, 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'           => '',
@@ -355,7 +362,6 @@ $self->{texts} = {
   'Attachment name'             => '',
   'Attachments'                 => '',
   'Attempt to call an undefined sub named \'%s\'' => '',
-  'Attention: Here will be generated a lot of CB/OB transactions.' => '',
   'Audit Control'               => '',
   'Aug'                         => '',
   'August'                      => '',
@@ -388,8 +394,10 @@ $self->{texts} = {
   'Background jobs and task server' => '',
   'Balance'                     => '',
   'Balance Sheet'               => '',
+  'Balance accounts'            => '',
   'Balance sheet date'          => '',
   'Balance startdate method'    => '',
+  'Balance with CB'             => '',
   'Balances'                    => '',
   'Balancing'                   => '',
   'Bank'                        => '',
@@ -473,6 +481,7 @@ $self->{texts} = {
   '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'          => '',
@@ -494,8 +503,6 @@ $self->{texts} = {
   'CANCELED'                    => '',
   'CB Transaction'              => '',
   'CB Transactions'             => '',
-  'CB date #1 is higher than OB date #2. Please select again.' => '',
-  'CB/OB Transactions'          => '',
   'CN'                          => '',
   'CR'                          => '',
   'CSS style for pictures'      => '',
@@ -507,6 +514,7 @@ $self->{texts} = {
   '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' => '',
@@ -524,6 +532,7 @@ $self->{texts} = {
   '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 change transaction in a closed period!' => '',
   'Cannot check correct WebDAV folder' => '',
   'Cannot delete account!'      => '',
   'Cannot delete customer!'     => '',
@@ -564,10 +573,12 @@ $self->{texts} = {
   '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 unlink payment for a closed period!' => '',
+  'Carry over account for year-end closing' => '',
   'Carry over shipping address' => '',
   'Cash'                        => '',
   'Cash accounting'             => '',
@@ -638,6 +649,7 @@ $self->{texts} = {
   'Close Window'                => '',
   'Close window'                => '',
   'Closed'                      => '',
+  'Closing Balance'             => '',
   'Collective Orders only work for orders from one customer!' => '',
   'Column name'                 => '',
   'Comma'                       => '',
@@ -646,6 +658,7 @@ $self->{texts} = {
   'Company'                     => '',
   'Company Name'                => '',
   'Company name'                => '',
+  'Company name and address'    => '',
   'Company settings'            => '',
   'Compare to'                  => '',
   'Complexities'                => '',
@@ -656,9 +669,11 @@ $self->{texts} = {
   '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)'    => '',
@@ -684,6 +699,8 @@ $self->{texts} = {
   'Correct taxkey'              => '',
   'Cost Center'                 => '',
   'Costs'                       => '',
+  'Could not create new project #1' => '',
+  'Could not extract 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' => '',
@@ -704,6 +721,8 @@ $self->{texts} = {
   'Create Date'                 => '',
   'Create HTML'                 => '',
   'Create PDF'                  => '',
+  'Create ZUGFeRD invoices'     => '',
+  'Create ZUGFeRD invoices in test mode' => '',
   'Create a new background job' => '',
   'Create a new client'         => '',
   'Create a new delivery term'  => '',
@@ -757,6 +776,7 @@ $self->{texts} = {
   'Create new version'          => '',
   'Create one from the context menu by right-clicking on this text.' => '',
   'Create order'                => '',
+  'Create sales invoices with ZUGFeRD data' => '',
   'Create tables'               => '',
   'Created by'                  => '',
   'Created for'                 => '',
@@ -939,6 +959,7 @@ $self->{texts} = {
   'Default template format'     => '',
   'Default transfer delivery order' => '',
   'Default transfer invoice'    => '',
+  'Default transfer invoice with charge number' => '',
   'Default transport article number' => '',
   'Default unit'                => '',
   'Default value'               => '',
@@ -984,6 +1005,7 @@ $self->{texts} = {
   'Delivery terms'              => '',
   'Delivery terms (database ID)' => '',
   'Delivery terms (name)'       => '',
+  'DeliveryOrder'               => '',
   'Denmark'                     => '',
   'Department'                  => '',
   'Department (database ID)'    => '',
@@ -1025,6 +1047,7 @@ $self->{texts} = {
   'Displayable Name Preferences' => '',
   'Do not change the tax rate of taxkey 0.' => '',
   'Do not check for duplicates' => '',
+  'Do not create ZUGFeRD invoices' => '',
   'Do not link to a project.'   => '',
   'Do not modify this position' => '',
   'Do not run the task server for this client' => '',
@@ -1032,12 +1055,12 @@ $self->{texts} = {
   'Do not set this bin'         => '',
   'Do not set this comment'     => '',
   'Do not set this warehouse'   => '',
-  'Do you really want do continue?' => '',
   '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?' => '',
@@ -1049,8 +1072,6 @@ $self->{texts} = {
   '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 save?' => '',
-  'Do you really want to send by mail?' => '',
   '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?' => '',
@@ -1171,6 +1192,7 @@ $self->{texts} = {
   'Edit Vendor'                 => '',
   'Edit Vendor Invoice'         => '',
   'Edit Warehouse'              => '',
+  'Edit ZUGFeRD notes'          => '',
   'Edit acceptance status'      => '',
   'Edit additional articles'    => '',
   'Edit all drafts'             => '',
@@ -1188,6 +1210,7 @@ $self->{texts} = {
   'Edit department'             => '',
   'Edit file'                   => '',
   'Edit general settings'       => '',
+  'Edit greeting'               => '',
   'Edit greetings'              => '',
   'Edit language'               => '',
   'Edit note'                   => '',
@@ -1227,6 +1250,7 @@ $self->{texts} = {
   'Edit the request_quotation'  => '',
   'Edit the sales_order'        => '',
   'Edit the sales_quotation'    => '',
+  'Edit title'                  => '',
   'Edit units'                  => '',
   'Edit user signature'         => '',
   'Editable'                    => '',
@@ -1254,6 +1278,7 @@ $self->{texts} = {
   'Equity'                      => '',
   'Erfolgsrechnung'             => '',
   'Error'                       => '',
+  '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.' => '',
@@ -1262,6 +1287,8 @@ $self->{texts} = {
   '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 with default taxzone'  => '',
   'Error!'                      => '',
   'Error: #1'                   => '',
@@ -1273,6 +1300,7 @@ $self->{texts} = {
   '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'          => '',
@@ -1299,11 +1327,17 @@ $self->{texts} = {
   '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'  => '',
@@ -1332,8 +1366,10 @@ $self->{texts} = {
   '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:'   => '',
   'Ertrag'                      => '',
   'Ertrag prozentual'           => '',
   'Escape character'            => '',
@@ -1499,7 +1535,6 @@ $self->{texts} = {
   'General ledger transactions can only be changed on the day they are posted.' => '',
   'General settings'            => '',
   'Generate and print sales delivery orders' => '',
-  'Generic Tax Report'          => '',
   'Germany'                     => '',
   'Get shoporders'              => 'Get and process orders from a web shop',
   'Git revision: #1, #2 #3'     => '',
@@ -1576,6 +1611,7 @@ $self->{texts} = {
   '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 enabled 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 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.' => '',
@@ -1584,12 +1620,14 @@ $self->{texts} = {
   '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 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 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.' => '',
@@ -1600,6 +1638,7 @@ $self->{texts} = {
   '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'     => '',
   'Illegal characters have been removed from the following fields: #1' => '',
   'Illegal date'                => '',
   'Image'                       => '',
@@ -1611,6 +1650,7 @@ $self->{texts} = {
   'Import CSV'                  => '',
   'Import Status'               => '',
   'Import a MT940 file:'        => '',
+  'Import a ZUGFeRD file:'      => '',
   'Import all'                  => '',
   'Import documents from #1'    => '',
   'Import file'                 => '',
@@ -1636,6 +1676,7 @@ $self->{texts} = {
   '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'                   => '',
@@ -1667,6 +1708,7 @@ $self->{texts} = {
   'Introduction of clients'     => '',
   'Inv. Duedate'                => '',
   'Invalid'                     => '',
+  '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.'       => '',
@@ -1711,6 +1753,7 @@ $self->{texts} = {
   'Invoices with payments cannot be canceled.' => '',
   'Invoices, Credit Notes & AR Transactions' => '',
   'Is Searchable'               => '',
+  'Is sales'                    => '',
   '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!' => '',
@@ -1808,6 +1851,7 @@ $self->{texts} = {
   'List Printers'               => '',
   'List Transactions'           => '',
   'List Users, Clients and User Groups' => '',
+  'List all rows'               => '',
   'List current background jobs' => '',
   'List export'                 => '',
   'List of bank collections'    => '',
@@ -1828,6 +1872,7 @@ $self->{texts} = {
   'Local account number'        => '',
   'Local bank account'          => '',
   'Local bank code'             => '',
+  'Lock'                        => '',
   'Lock System'                 => '',
   'Lock and unlock installation' => '',
   'Lock bookings'               => '',
@@ -1845,6 +1890,8 @@ $self->{texts} = {
   'Long Description (quotations & orders)' => '',
   'Long Description for invoices' => '',
   'Long Description for quotations & orders' => '',
+  'Loss'                        => '',
+  'Loss carried forward account' => '',
   'Luxembourg'                  => '',
   'MAILED'                      => '',
   'MD'                          => '',
@@ -1880,6 +1927,7 @@ $self->{texts} = {
   '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'   => '',
@@ -1906,6 +1954,7 @@ $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' => '',
@@ -1937,6 +1986,7 @@ $self->{texts} = {
   '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 charge number!'         => '',
   'Negative reductions are possible to model price increases.' => '',
   'Neither sections nor function blocks have been created yet.' => '',
   'Net Income Statement'        => '',
@@ -1971,6 +2021,7 @@ $self->{texts} = {
   'Next run at'                 => '',
   'No'                          => '',
   'No 1:n or n:1 relation'      => '',
+  'No AP Record Template for this vendor found, please add one' => '',
   'No AP template was found.'   => '',
   'No Company Address given'    => '',
   'No Company Name given'       => '',
@@ -1979,6 +2030,7 @@ $self->{texts} = {
   'No Journal'                  => '',
   'No Shopdescription'          => '',
   'No Shopimages'               => '',
+  'No VAT Info for this ZUGFeRD invoice, please ask your vendor to add this for his ZUGFeRD data.' => '',
   'No Vendor was found matching the search parameters.' => '',
   'No action defined.'          => '',
   'No article has been selected yet.' => '',
@@ -1986,9 +2038,11 @@ $self->{texts} = {
   'No assembly has been selected yet.' => '',
   'No background job has been created yet.' => '',
   'No bank account chosen!'     => '',
+  'No bank account flagged for ZUGFeRD 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 bins have been added to this warehouse yet.' => '',
+  'No carry-over chart configured!' => '',
   'No changes since previous version.' => '',
   'No clients have been created yet.' => '',
   'No contact selected to delete' => '',
@@ -2017,7 +2071,6 @@ $self->{texts} = {
   'No groups have been created yet.' => '',
   'No internal phone extensions have been configured yet.' => '',
   'No invoices have been selected.' => '',
-  'No or an unknown authenticantion module specified in "config/kivitendo.conf".' => '',
   'No part was selected.'       => '',
   'No payment term has been created yet.' => '',
   'No picture has been uploaded' => '',
@@ -2026,11 +2079,12 @@ $self->{texts} = {
   '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 revert available.'        => '',
   'No search results found!'    => '',
   'No sections created yet'     => '',
   'No sections have been created so far.' => '',
@@ -2067,6 +2121,7 @@ $self->{texts} = {
   'Not Discountable'            => '',
   'Not delivered'               => '',
   'Not done yet'                => '',
+  'Not enough in stock for the serial number #1' => '',
   'Not obsolete'                => '',
   'Note'                        => '',
   'Note that parameter names must not be quoted.' => '',
@@ -2106,7 +2161,6 @@ $self->{texts} = {
   'Number pages'                => '',
   'Number variables: \'PRECISION=n\' forces numbers to be shown with exactly n decimal places.' => '',
   'OB Transaction'              => '',
-  'OB Transactions'             => '',
   'Objects have been imported.' => '',
   'Obsolete'                    => '',
   'Oct'                         => '',
@@ -2119,14 +2173,13 @@ $self->{texts} = {
   'On Hand'                     => '',
   'On Order'                    => '',
   'On the next page the type of all variables can be set.' => '',
-  'One OB-transaction'          => '',
-  'One SB-transaction'          => '',
   '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' => '',
   '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 Warnings and Errors'    => '',
   'Only applies if the previous is set to true. When filling up unlinked positions, consider them matches if ALL of these fields match. For example, in a business with variants that are defined by a special description, description needs to be part of the identity. If delivering several similar order positions by delivery date is common, reqdate should be included in the identity. Serialnumber is useful when the serialnumber in the order and delivery order has to match.' => '',
   'Only booked accounts'        => '',
   'Only due follow-ups'         => '',
@@ -2164,6 +2217,7 @@ $self->{texts} = {
   'Order probability & expected billing date' => '',
   'Order value periodicity'     => '',
   'Order/Item row name'         => '',
+  'Order/Item/Stock row name'   => '',
   'Order/RFQ Number'            => '',
   'OrderItem'                   => '',
   'Ordered'                     => '',
@@ -2210,6 +2264,7 @@ $self->{texts} = {
   '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 (database ID)'          => '',
@@ -2251,6 +2306,7 @@ $self->{texts} = {
   'Payables'                    => '',
   'Payment'                     => '',
   'Payment / Delivery Options'  => '',
+  'Payment Date'                => '',
   'Payment Reminder'            => '',
   'Payment Terms'               => '',
   'Payment Terms missing in row ' => '',
@@ -2292,11 +2348,13 @@ $self->{texts} = {
   '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: ' => '',
   '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.' => '',
@@ -2376,6 +2434,7 @@ $self->{texts} = {
   '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' => '',
   'Preview'                     => '',
@@ -2386,6 +2445,7 @@ $self->{texts} = {
   'Price #1'                    => '',
   'Price Factor'                => '',
   'Price Factors'               => '',
+  'Price List'                  => '',
   'Price Rule'                  => '',
   'Price Rules'                 => '',
   'Price Source'                => '',
@@ -2441,6 +2501,9 @@ $self->{texts} = {
   'Production'                  => 'Production',
   'Production (typeabbreviation)' => 'W',
   'Productivity'                => '',
+  'Profit'                      => '',
+  'Profit and loss accounts'    => '',
+  'Profit carried forward account' => '',
   'Profit determination'        => '',
   'Proforma Invoice'            => '',
   'Program'                     => '',
@@ -2685,7 +2748,6 @@ $self->{texts} = {
   'SEPA strings'                => '',
   'SQL query'                   => '',
   'SWIFT MT940 format'          => '',
-  'Saldo'                       => '',
   'Saldo Credit'                => '',
   'Saldo Debit'                 => '',
   'Saldo neu'                   => '',
@@ -2774,6 +2836,9 @@ $self->{texts} = {
   'Search AR Aging'             => '',
   'Search bank transactions'    => '',
   'Search contacts'             => '',
+  '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'           => '',
@@ -2786,7 +2851,6 @@ $self->{texts} = {
   'Select Mulit-Item Options'   => '',
   'Select a Customer'           => '',
   'Select a period'             => '',
-  'Select charts for which the CB/OB transactions want to be posted.' => '',
   'Select federal state...'     => '',
   'Select file to upload'       => '',
   'Select from one of the items below' => '',
@@ -2949,6 +3013,7 @@ $self->{texts} = {
   '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 logging in' => '',
   'Show »not delivered qty/value« column in sales and purchase orders' => '',
@@ -2992,6 +3057,7 @@ $self->{texts} = {
   'Start of year'               => '',
   'Start process'               => '',
   'Start the correction assistant' => '',
+  'Startdate method'            => '',
   'Startdate_coa'               => '',
   'Starting Balance'            => '',
   'Starting balance'            => '',
@@ -3014,7 +3080,9 @@ $self->{texts} = {
   'Stock Local/Shop'            => '',
   'Stock Qty for Date'          => '',
   'Stock for part #1'           => '',
+  'Stock levels'                => '',
   'Stock value'                 => '',
+  'StockInfo'                   => '',
   'Stocked Qty'                 => '',
   'Stocktaking'                 => '',
   'Stocktaking History'         => '',
@@ -3033,6 +3101,8 @@ $self->{texts} = {
   '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' => '',
@@ -3045,10 +3115,8 @@ $self->{texts} = {
   'Subtotals per quarter'       => '',
   'Such entries cannot be exported into the DATEV format and have to be fixed as well.' => '',
   'Suggested invoice'           => '',
-  'Sum CB Transactions'         => '',
   'Sum Credit'                  => '',
   'Sum Debit'                   => '',
-  'Sum OB Transactions'         => '',
   'Sum for'                     => '',
   'Sum for #1'                  => '',
   'Sum for section'             => '',
@@ -3087,7 +3155,6 @@ $self->{texts} = {
   'Tax Office Preferences'      => '',
   'Tax Percent is a number between 0 and 100' => '',
   'Tax Period'                  => '',
-  'Tax Position'                => '',
   'Tax collected'               => '',
   'Tax deleted!'                => '',
   'Tax number'                  => '',
@@ -3112,7 +3179,6 @@ $self->{texts} = {
   'Taxkey_coa'                  => '',
   'Taxkeys and Taxreport Preferences' => '',
   'Taxlink_coa'                 => '',
-  'Taxnumber'                   => '',
   'Taxrate missing!'            => '',
   'Taxzones'                    => '',
   'Tel'                         => '',
@@ -3163,7 +3229,15 @@ $self->{texts} = {
   '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 ZUGFeRD data.' => '',
+  'The ZUGFeRD XML invoice was not found.' => '',
+  'The ZUGFeRD invoice data cannot be generated because the data validation failed.' => '',
+  'The ZUGFeRD notes have been saved.' => '',
+  'The ZUGFeRD version used is not supported.' => '',
   '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.' => '',
@@ -3216,6 +3290,7 @@ $self->{texts} = {
   '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.' => '',
@@ -3225,8 +3300,11 @@ $self->{texts} = {
   '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.' => '',
@@ -3234,6 +3312,7 @@ $self->{texts} = {
   'The custom variable has been saved.' => '',
   'The custom variable is in use and cannot be deleted.' => '',
   'The customer name is missing.' => '',
+  '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.' => '',
@@ -3273,6 +3352,8 @@ $self->{texts} = {
   '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'    => '',
@@ -3506,7 +3587,6 @@ $self->{texts} = {
   'There was an error saving the draft' => '',
   'There was an error saving the letter' => '',
   'There was an error saving the letter draft' => '',
-  'There will be two transactions done:' => '',
   '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.' => '',
@@ -3514,6 +3594,8 @@ $self->{texts} = {
   '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.' => '',
@@ -3590,7 +3672,6 @@ $self->{texts} = {
   'Time estimate'               => '',
   'Time period for the analysis:' => '',
   'Time/cost estimate actions'  => '',
-  'Timerange'                   => '',
   'Timestamp'                   => '',
   'Tired of copying always nice phrases for this message? Click here to use the new preset message option!' => '',
   'Title'                       => '',
@@ -3689,6 +3770,7 @@ $self->{texts} = {
   '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'           => '',
@@ -3704,6 +3786,7 @@ $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'          => '',
@@ -3720,6 +3803,7 @@ $self->{texts} = {
   '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' => '',
@@ -3760,14 +3844,22 @@ $self->{texts} = {
   '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 default booking group because setting is \'all\'' => '',
   'Use default booking group because wanted is missing' => '',
   'Use default warehouse for assembly transfer' => '',
   'Use existing templates'      => '',
   'Use fill up when calculating shipped quantities?' => '',
+  'Use for ZUGFeRD'             => '',
   'Use linked items'            => '',
   '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' => '',
@@ -3831,6 +3923,7 @@ $self->{texts} = {
   'View background job execution result' => '',
   'View sent email'             => '',
   'View warehouse content'      => '',
+  'View/edit all employees purchase documents' => '',
   'View/edit all employees sales documents' => '',
   'Von Konto: '                 => '',
   'WHJournal'                   => 'Warehouse journal',
@@ -3850,6 +3943,7 @@ $self->{texts} = {
   'Warn before saving orders without a delivery date' => '',
   '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'                => '',
@@ -3895,6 +3989,10 @@ $self->{texts} = {
   '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'                         => '',
@@ -3917,10 +4015,12 @@ $self->{texts} = {
   '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 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.' => '',
@@ -3944,6 +4044,7 @@ $self->{texts} = {
   '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'              => '',
@@ -3951,11 +4052,16 @@ $self->{texts} = {
   'Your download does not exist anymore. Please re-run the DATEV export assistant.' => '',
   'Your import is being processed.' => '',
   'Your target quantity will be added to the stocked quantity.' => '',
+  'ZUGFeRD import'              => '',
+  'ZUGFeRD invoice'             => '',
+  'ZUGFeRD notes for each invoice' => '',
   'Zeitraum'                    => '',
   'Zero amount posting!'        => '',
   'Zip'                         => '',
   'Zip, City'                   => '',
   'Zipcode'                     => '',
+  'Zipcode and city'            => '',
+  'ZugFeRD Import'              => '',
   '[email]'                     => '',
   'absolute'                    => '',
   'account_description'         => '',
@@ -3983,19 +4089,20 @@ $self->{texts} = {
   'bank_collection_payment_list_#1' => '',
   'bank_transfer_payment_list_#1' => '',
   'banktransfers'               => '',
+  'basis for stock value'       => '',
   'bestbefore #1'               => '',
   'bin_list'                    => '',
   'bis'                         => '',
   'brutto'                      => '',
   'building data'               => '',
   'building report'             => '',
+  'can only parse a pdf file'   => '',
   'cash'                        => '',
   'chargenumber #1'             => '',
   'chart_of_accounts'           => '',
   'cleared'                     => '',
   'click here to edit cvars'    => '',
   'close'                       => '',
-  'close chart'                 => '',
   'closed'                      => '',
   'companylogo_subtitle'        => '',
   'config/kivitendo.conf: Key "DB_config" is missing.' => '',
@@ -4032,6 +4139,8 @@ $self->{texts} = {
   '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'                      => '',
@@ -4055,7 +4164,6 @@ $self->{texts} = {
   'from \'#1\' imported Files'  => '',
   'from (time)'                 => '',
   'general_ledger_list'         => '',
-  'generate cb/ob transactions for selected charts' => '',
   'generated Files'             => '',
   'gobd-#1-#2.zip'              => '',
   'h'                           => '',
@@ -4095,10 +4203,12 @@ $self->{texts} = {
   'male'                        => '',
   'max filesize'                => '',
   'missing'                     => '',
+  'missing file for action import' => '',
   'missing_br'                  => 'missing',
   'month'                       => '',
   'monthly'                     => '',
   'more'                        => '',
+  'natural person'              => '',
   'netto'                       => '',
   'never'                       => '',
   'new order controller'        => '',
index 0712bc5..7481729 100644 (file)
@@ -29,7 +29,7 @@ order=< > \n
 \n=<br>
 
 [Template/LaTeX]
-order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © \xad \xa0 ➔ → ← | − ≤ ≥ ‐ ​ Ω μ Δ ~
+order=\\ <pagebreak> & \n \r " $ <bullet> % _ # ^ { } < > £ ± ² ³ ° § ® © ~ \xad \xa0 ➔ → ← | − ≤ ≥ ‐ ​ Ω μ Δ ‑
 \\=\\textbackslash\s
 <pagebreak>=
 "=''
@@ -71,6 +71,7 @@ _=\\_
 Δ=$\\Delta$
 Ω=$\\Omega$
 ~={\\raisebox{0.5ex}{\\texttildelow}}
+‑={}-{}
 
 [Template/OpenDocument]
 order=& < > " ' \x80 \n \r
index 8c69e4b..f223a1d 100644 (file)
@@ -76,7 +76,7 @@
   name: Add Assortment
   icon: assortment_add
   order: 550
-  access: part_service_assembly_edit & client/feature_experimental
+  access: part_service_assembly_edit & client/feature_experimental_assortment
   params:
     action: Part/add_assortment
 - parent: master_data
   params:
     action: GoBD/filter
 - parent: general_ledger
-  id: general_ledger_cbob_transactions
-  name: CB/OB Transactions
+  id: year_end_closing
+  name: Year-end closing
   icon: cbob
   order: 470
   access: general_ledger
   params:
-    action: YearEndTransactions/filter
+    action: YearEndTransactions/form
+- parent: general_ledger
+  id: zugferd_import
+  name: ZugFeRD Import
+  icon: cbob
+  order: 485
+  access: ap_transactions
+  params:
+    action: ZUGFeRD/upload_zugferd
 - parent: general_ledger
   id: general_ledger_reports
   name: Reports
   params:
     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
   params:
     action: SimpleSystemSetting/list
     type: business
+- parent: system
+  id: system_contact_titles
+  name: Contact Titles
+  order: 1420
+  params:
+    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
   module: generictranslations.pl
   params:
     action: edit_sepa_strings
+- parent: system_languages_and_translations
+  id: system_languages_and_translations_zugferd_notes
+  name: 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
   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
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
index 77c9cb7..c3ef1e7 100755 (executable)
@@ -86,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
index c583ecc..b2a3c0d 100755 (executable)
@@ -145,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;
@@ -249,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 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 fa60838..db4ae24 100755 (executable)
@@ -13,6 +13,7 @@ BEGIN {
 
 use strict;
 use Getopt::Long;
+use List::MoreUtils qw(uniq);
 use Pod::Usage;
 use Term::ANSIColor;
 use Text::Wrap;
@@ -81,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 configuration.')
+  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();
 }
@@ -121,7 +122,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
   }
 }
@@ -145,9 +146,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 {
@@ -156,7 +176,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/';
@@ -228,7 +253,12 @@ sub check_aqbanking {
     my ($label,$version)  = split /:/,$shell_out;
     if ( $label && $label eq ' AqBanking-CLI' ) {
       chop $version;
-      print_line($line, $version, 'green');
+      my ($number_version) = $version =~ /(\d+)/;
+      if ($number_version < 6) {
+        print_line($line, "Requires at least version 6, current version is " . $version, 'red');
+      } else {
+        print_line($line, $version, 'green');
+      }
     } else {
       print_line($line, 'not installed','red');
       my %modinfo = ( name => 'aqbanking' );
@@ -259,7 +289,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 $" = $/;
@@ -329,7 +359,7 @@ __END__
 
 =head1 NAME
 
-scripts/installation_check.pl - check kivitendo dependancies
+scripts/installation_check.pl - check kivitendo dependencies
 
 =head1 SYNOPSIS
 
@@ -361,11 +391,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>
 
@@ -397,7 +427,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 c9494c8..3d28149 100755 (executable)
@@ -322,6 +322,7 @@ sub gd_run {
     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';
index 5ee5b5e..f9db7d0 100644 (file)
@@ -1,6 +1,6 @@
 # @tag: all_drafts_edit
 # @description: Zugriffsrecht auf alle Entwürfe
-# @depends: release_3_4_0
+# @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;
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';
index 8987f1f..d803cbe 100644 (file)
@@ -1,6 +1,6 @@
 # @tag: purchase_letter_rights
 # @description: Neue Rechte für Lieferantenbriefe
-# @depends: release_3_2_0 sales_letter_rights
+# @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;
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/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/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';
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/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/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/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/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_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_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/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/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_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/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_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_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_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_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..ab1d1a4
--- /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 EXISTS
+  (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();
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();
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/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
+  )
+);
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/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_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/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/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));
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/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_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);
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/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/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;
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 a927503..4e739b7 100755 (executable)
@@ -86,7 +86,7 @@ sub drop_and_create_database {
 sub report_success {
   $dbh->disconnect;
   $superuser_dbh->disconnect if $superuser_dbh;
-  ok(1, "Database has been setup sucessfully.");
+  ok(1, "Database has been set up successfully.");
   done_testing();
 }
 
index bc511a4..71b7e58 100644 (file)
@@ -43,6 +43,7 @@ existant
 fomr
 invoce
 lenght
+occured
 paramater
 pirce
 postition
index dfb8756..c5583f1 100644 (file)
--- a/t/ar/ar.t
+++ b/t/ar/ar.t
@@ -1,5 +1,5 @@
 use strict;
-use Test::More tests => 6;
+use Test::More tests => 7;
 
 use lib 't';
 use Support::TestSetup;
@@ -56,6 +56,7 @@ if ( $inv ) {
   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";
 }
@@ -92,7 +93,8 @@ sub _ar {
 
   my $amount       = $params{amount}       || croak "ar needs param amount";
   my $customer     = $params{customer}     || croak "ar needs param customer";
-  my $date         = $params{date}         || DateTime->today;
+  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
@@ -100,7 +102,8 @@ sub _ar {
       invoice          => 0,
       amount           => $amount,
       netamount        => $amount,
-      transdate        => $date,
+      transdate        => $transdate,
+      gldate           => $gldate,
       taxincluded      => 'f',
       customer_id      => $customer->id,
       taxzone_id       => $customer->taxzone_id,
@@ -144,14 +147,16 @@ sub _ar_with_tax {
 
   my $amount       = $params{amount}       || croak "ar needs param amount";
   my $customer     = $params{customer}     || croak "ar needs param customer";
-  my $date         = $params{date}         || DateTime->today;
+  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        => $date,
+    transdate        => $transdate,
+    gldate           => $gldate,
     taxincluded      => 'f',
     customer_id      => $customer->id,
     taxzone_id       => $customer->taxzone_id,
index 8b38424..e2bb552 100644 (file)
@@ -31,9 +31,10 @@ 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 ($transdate1, $transdate2, $currency);
+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 {
 
@@ -116,8 +117,11 @@ sub reset_state {
 
   clear_up();
 
-  $transdate1 = DateTime->today;
-  $transdate2 = DateTime->today->add(days => 5);
+  $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\%";
@@ -141,7 +145,7 @@ sub reset_state {
     iban                      => 'DE12500105170648489890',
     bic                       => 'TESTBIC',
     account_number            => '648489890',
-    mandate_date_of_signature => $transdate1,
+    mandate_date_of_signature => $dt,
     mandator_id               => 'foobar',
     bank                      => 'Geizkasse',
     bank_code                 => 'G1235',
@@ -180,7 +184,7 @@ sub test_ar_transaction {
       invnumber    => $params{invnumber} || undef, # let it use its own invnumber
       amount       => $amount,
       netamount    => $netamount,
-      transdate    => $transdate1,
+      transdate    => $dt,
       taxincluded  => $params{taxincluded } || 0,
       customer_id  => $customer->id,
       taxzone_id   => $customer->taxzone_id,
@@ -221,7 +225,7 @@ sub test_ap_transaction {
     invnumber    => $params{invnumber} || $testname,
     amount       => $amount,
     netamount    => $netamount,
-    transdate    => $transdate1,
+    transdate    => $dt,
     taxincluded  => 0,
     vendor_id    => $vendor->id,
     taxzone_id   => $vendor->taxzone_id,
@@ -257,7 +261,9 @@ sub test1 {
 
   $ar_transaction = test_ar_transaction(invnumber => 'salesinv1');
 
-  my $bt = create_bank_transaction(record => $ar_transaction) or die "Couldn't create bank_transaction";
+  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 ]
@@ -283,6 +289,8 @@ sub test_skonto_exact {
 
   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";
 
@@ -317,6 +325,8 @@ sub test_bt_error {
 
   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} = {
@@ -357,6 +367,8 @@ sub test_two_invoices {
   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";
 
@@ -397,6 +409,8 @@ sub test_one_inv_and_two_invoices_with_skonto_exact {
 
   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";
 
@@ -436,6 +450,8 @@ sub test_overpayment {
   # 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";
 
@@ -467,11 +483,14 @@ sub test_overpayment_with_partialpayment {
 
   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     => DateTime->today->add(days => 5),
+                                     transdate     => $dt_5,
+                                     valutadate    => $dt_5,
                                      bank_chart_id => $bank->id,
                                     ) or die "Couldn't create bank_transaction";
 
@@ -505,6 +524,8 @@ sub test_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";
 
@@ -678,6 +699,7 @@ sub test_credit_note {
   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),
@@ -686,7 +708,7 @@ sub test_credit_note {
   my $bt            = create_bank_transaction(record        => $credit_note,
                                                                 amount        => $credit_note->amount,
                                                                 bank_chart_id => $bank->id,
-                                                                transdate     => DateTime->today->add(days => 10),
+                                                                transdate     => $dt_10,
                                                                );
   my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($credit_note);
   is($agreement, 13, "points for credit note ok");
@@ -715,7 +737,7 @@ sub test_neg_ap_transaction {
     invnumber    => $params{invnumber} || 'test_neg_ap_transaction',
     amount       => $amount,
     netamount    => $netamount,
-    transdate    => $transdate1,
+    transdate    => $dt,
     taxincluded  => 0,
     vendor_id    => $vendor->id,
     taxzone_id   => $vendor->taxzone_id,
@@ -738,7 +760,7 @@ sub test_neg_ap_transaction {
   my $bt            = create_bank_transaction(record        => $invoice,
                                               amount        => $invoice->amount,
                                               bank_chart_id => $bank->id,
-                                              transdate     => DateTime->today->add(days => 10),
+                                              transdate     => $dt_10,
                                                                );
 
   my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($invoice);
@@ -770,7 +792,7 @@ sub test_two_neg_ap_transaction {
     invnumber    => 'test_neg_ap_transaction',
     amount       => $amount,
     netamount    => $netamount,
-    transdate    => $transdate1,
+    transdate    => $dt,
     taxincluded  => 0,
     vendor_id    => $vendor->id,
     taxzone_id   => $vendor->taxzone_id,
@@ -797,7 +819,7 @@ sub test_two_neg_ap_transaction {
     invnumber    => 'test_neg_ap_transaction_two',
     amount       => $amount_two,
     netamount    => $netamount_two,
-    transdate    => $transdate1,
+    transdate    => $dt,
     taxincluded  => 0,
     vendor_id    => $vendor->id,
     taxzone_id   => $vendor->taxzone_id,
@@ -821,7 +843,7 @@ sub test_two_neg_ap_transaction {
   my $bt            = create_bank_transaction(record        => $invoice_two,
                                               amount        => $invoice_two->amount + $invoice->amount,
                                               bank_chart_id => $bank->id,
-                                              transdate     => DateTime->today->add(days => 10),
+                                              transdate     => $dt_10,
                                                                );
   # my ($agreement, $rule_matches) = $bt->get_agreement_with_invoice($invoice_two);
   # is($agreement, 15, "points for negative ap transaction ok");
@@ -858,7 +880,7 @@ sub test_ap_payment_transaction {
     invnumber    => $params{invnumber} || $testname,
     amount       => $amount,
     netamount    => $netamount,
-    transdate    => $transdate1,
+    transdate    => $dt,
     taxincluded  => 0,
     vendor_id    => $vendor->id,
     taxzone_id   => $vendor->taxzone_id,
@@ -881,7 +903,7 @@ sub test_ap_payment_transaction {
   my $bt            = create_bank_transaction(record        => $invoice,
                                               amount        => $invoice->amount,
                                               bank_chart_id => $bank->id,
-                                              transdate     => DateTime->today->add(days => 10),
+                                              transdate     => $dt_10,
                                              );
   $::form->{invoice_ids} = {
     $bt->id => [ $invoice->id ]
@@ -911,7 +933,7 @@ sub test_ap_payment_part_transaction {
     invnumber    => $params{invnumber} || $testname,
     amount       => $amount,
     netamount    => $netamount,
-    transdate    => $transdate1,
+    transdate    => $dt,
     taxincluded  => 0,
     vendor_id    => $vendor->id,
     taxzone_id   => $vendor->taxzone_id,
@@ -934,7 +956,7 @@ sub test_ap_payment_part_transaction {
   my $bt            = create_bank_transaction(record        => $invoice,
                                               amount        => $invoice->amount-100,
                                               bank_chart_id => $bank->id,
-                                              transdate     => DateTime->today->add(days => 10),
+                                              transdate     => $dt_10,
                                              );
   $::form->{invoice_ids} = {
     $bt->id => [ $invoice->id ]
@@ -954,7 +976,7 @@ sub test_ap_payment_part_transaction {
   my $bt2           = create_bank_transaction(record        => $invoice,
                                               amount        => 100,
                                               bank_chart_id => $bank->id,
-                                              transdate     => DateTime->today->add(days => 10),
+                                              transdate     => $dt_10,
                                              );
   $::form->{invoice_ids} = {
     $bt2->id => [ $invoice->id ]
@@ -984,6 +1006,7 @@ sub test_neg_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),
                     ]
@@ -991,7 +1014,8 @@ sub test_neg_sales_invoice {
   my $bt            = create_bank_transaction(record        => $neg_sales_inv,
                                                                 amount        => $neg_sales_inv->amount,
                                                                 bank_chart_id => $bank->id,
-                                                                transdate     => DateTime->today,
+                                                                transdate     => $dt,
+                                                                valutadate    => $dt,
                                                                );
   $::form->{invoice_ids} = {
     $bt->id => [ $neg_sales_inv->id ]
@@ -1012,9 +1036,9 @@ sub test_bt_rule1 {
 
   my $testname = 'test_bt_rule1';
 
-  $ar_transaction = test_ar_transaction(invnumber => 'bt_rule1');
+  $ar_transaction = test_ar_transaction(invnumber => 'bt_rule1', transdate => $dt);
 
-  my $bt = create_bank_transaction(record => $ar_transaction) or die "Couldn't create bank_transaction";
+  my $bt = create_bank_transaction(record => $ar_transaction, transdate => $dt) or die "Couldn't create bank_transaction";
 
   $ar_transaction->load;
   $bt->load;
@@ -1022,9 +1046,7 @@ sub test_bt_rule1 {
   is($bt->invoice_amount     , '0.00000' , "$testname: bt invoice amount was not assigned");
 
   my $bt_controller = SL::Controller::BankTransaction->new;
-  $::form->{dont_render_for_test} = 1;
-  $::form->{filter}{bank_account} = $bank_account->id;
-  my ( $bt_transactions, $proposals ) = $bt_controller->action_list;
+  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");
@@ -1042,9 +1064,9 @@ sub test_sepa_export {
 
   my $testname = 'test_sepa_export';
 
-  $ar_transaction = test_ar_transaction(invnumber => 'sepa1');
+  $ar_transaction = test_ar_transaction(invnumber => 'sepa1', transdate => $dt);
 
-  my $bt  = create_bank_transaction(record => $ar_transaction) or die "Couldn't create bank_transaction";
+  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,
@@ -1075,9 +1097,7 @@ sub test_sepa_export {
   is($sei->amount            , '119.00000' , "$testname: sepa export amount ok");
 
   my $bt_controller = SL::Controller::BankTransaction->new;
-  $::form->{dont_render_for_test} = 1;
-  $::form->{filter}{bank_account} = $bank_account->id;
-  my ( $bt_transactions, $proposals ) = $bt_controller->action_list;
+  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");
@@ -1095,12 +1115,14 @@ sub test_two_banktransactions {
   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";
 
@@ -1131,6 +1153,7 @@ sub test_two_banktransactions {
   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";
 
@@ -1143,9 +1166,7 @@ sub test_two_banktransactions {
   #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;
-  $::form->{dont_render_for_test} = 1;
-  $::form->{filter}{bank_account} = $bank_account->id;
-  my ( $bt_transactions, $proposals ) = $bt_controller->action_list;
+  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");
@@ -1155,7 +1176,7 @@ sub test_two_banktransactions {
   # Jetzt gibt es zwei Kontobewegungen mit gleichen Punkten für eine Rechnung.
   # hier darf es auch keine Proposals geben
 
-  ( $bt_transactions, $proposals ) = $bt_controller->action_list;
+  ( $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
@@ -1166,7 +1187,7 @@ sub test_two_banktransactions {
   # hier darf es auch keine Proposals geben
   $bt3->update_attributes( purpose => "fuer Rechnung salesinv10000");
 
-  ( $bt_transactions, $proposals ) = $bt_controller->action_list;
+  ( $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
diff --git a/t/bank/cb_ob_transactions.t b/t/bank/cb_ob_transactions.t
deleted file mode 100644 (file)
index 472d333..0000000
+++ /dev/null
@@ -1,289 +0,0 @@
-use Test::More;
-
-use strict;
-
-use lib 't';
-use utf8;
-
-use Carp;
-use Support::TestSetup;
-use Test::Exception;
-use List::Util qw(sum);
-
-use SL::DB::Buchungsgruppe;
-use SL::DB::Currency;
-use SL::DB::Exchangerate;
-use SL::DB::Customer;
-use SL::DB::Vendor;
-use SL::DB::Employee;
-use SL::DB::Invoice;
-use SL::DB::Part;
-use SL::DB::Unit;
-use SL::DB::TaxZone;
-use SL::DB::BankAccount;
-use SL::DB::PaymentTerm;
-use SL::DB::PurchaseInvoice;
-use SL::DB::BankTransaction;
-use SL::DB::AccTransaction;
-use SL::Controller::YearEndTransactions;
-use Data::Dumper;
-
-my ($customer, $vendor, $currency_id, @parts, $unit, $employee, $tax, $tax7, $tax_9, $taxzone, $payment_terms, $bank_account);
-my ($transdate1, $transdate2, $currency);
-my ($ar_chart,$bank,$ar_amount_chart, $ap_chart, $ap_amount_chart, $saldo_chart);
-my ($ar_transaction, $ap_transaction);
-
-sub clear_up {
-
-  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::BankAccount->delete_all(all => 1);
-  SL::DB::Manager::AccTransaction->delete_all(all => 1);
-  SL::DB::Manager::GLTransaction->delete_all(all => 1);
-  SL::DB::Manager::PaymentTerm->delete_all(all => 1);
-  SL::DB::Manager::Currency->delete_all(where => [ name => 'CUR' ]);
-};
-
-
-# starting test:
-Support::TestSetup::login();
-
-reset_state(); # initialise customers/vendors/bank/currency/...
-
-test1();
-
-# remove all created data at end of test
-#clear_up();
-
-done_testing();
-
-###### functions for setting up data
-
-sub reset_state {
-  my %params = @_;
-
-  $params{$_} ||= {} for qw(unit customer part tax vendor);
-
-  clear_up();
-
-  $transdate1 = DateTime->today;
-  $transdate2 = DateTime->today->add(days => 5);
-
-  $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";
-  $tax_9           = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19, %{ $params{tax} }) || croak "No tax";
-
-  $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     = SL::DB::Customer->new(
-    name                      => 'Test Customer',
-    currency_id               => $currency_id,
-    taxzone_id                => $taxzone->id,
-    iban                      => 'DE12500105170648489890',
-    bic                       => 'TESTBIC',
-    account_number            => '648489890',
-    mandate_date_of_signature => $transdate1,
-    mandator_id               => 'foobar',
-    bank                      => 'Geizkasse',
-    depositor                 => 'Test Customer',
-    %{ $params{customer} }
-  )->save;
-
-  $payment_terms     =  SL::DB::PaymentTerm->new(
-    description      => 'payment',
-    description_long => 'payment',
-    terms_netto      => '30',
-    terms_skonto     => '5',
-    percent_skonto   => '0.05',
-    auto_calculation => 1,
-  )->save;
-
-  $vendor       = SL::DB::Vendor->new(
-    name        => 'Test Vendor',
-    currency_id => $currency_id,
-    taxzone_id  => $taxzone->id,
-    payment_id  => $payment_terms->id,
-    iban                      => 'DE12500105170648489890',
-    bic                       => 'TESTBIC',
-    account_number            => '648489890',
-    bank                      => 'Geizkasse',
-    depositor                 => 'Test Vendor',
-    %{ $params{vendor} }
-  )->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%
-  $saldo_chart     = SL::DB::Manager::Chart->find_by( accno => '9000' ); # Saldenvorträge
-
-}
-
-sub test_ar_transaction {
-  my (%params) = @_;
-  my $netamount = 100;
-  my $amount    = $params{amount} || $::form->round_amount(100 * 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    => $transdate1,
-      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 => $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');
-
-  return $invoice;
-};
-
-sub test_ap_transaction {
-  my (%params) = @_;
-  my $netamount = 100;
-  my $amount    = $::form->round_amount($netamount * 1.19,2);
-  my $invoice   = SL::DB::PurchaseInvoice->new(
-      invoice      => 0,
-      invnumber    => $params{invnumber} || 'test_ap_transaction',
-      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_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->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');
-
-  return $invoice;
-};
-
-###### test cases
-
-sub test1 {
-
-  my $testname = 'test1';
-
-  $ar_transaction = test_ar_transaction(invnumber => 'salesinv1');
-  $ap_transaction = test_ap_transaction(invnumber => 'purchaseinv1');
-  my $ar_transaction_2 = test_ar_transaction(invnumber => 'salesinv_2');
-
-  my $yt_controller = SL::Controller::YearEndTransactions->new;
-  my $report     = SL::ReportGenerator->new(\%::myconfig, $::form);
-
-  $::form->{"ob_date"} = DateTime->today->truncate(to => 'year')->add(years => 1)->to_kivitendo;
-  $::form->{"cb_date"} = DateTime->today->truncate(to => 'year')->add(years => 1)->add(days => -1)->to_kivitendo;
-  #print "ob_date=".$::form->{"ob_date"}." cb_date=".$::form->{"cb_date"}."\n";
-  $::form->{"cb_reference"} = 'SB-Buchung';
-  $::form->{"ob_reference"} = 'EB-Buchung';
-  $::form->{"cb_description"} = 'SB-Buchung Beschreibung';
-  $::form->{"ob_description"} = 'EB-Buchung Beschreibung';
-  $::form->{"cbob_chart"} = $saldo_chart->id;
-
-  $yt_controller->prepare_report($report);
-
-  ## check balance of charts
-
-  my $idx = 1;
-  foreach my $chart (@{ $yt_controller->charts }) {
-    my $balance = $yt_controller->get_balance($chart);
-    if ( $balance != 0 ) {
-      #print "chart_id=".$chart->id."balance=".$balance."\n";
-      is($balance , '-238.00000' , $chart->accno.' has right balance') if $chart->accno eq '1400';
-      is($balance ,  '-19.00000' , $chart->accno.' has right balance') if $chart->accno eq '1576';
-      is($balance ,  '119.00000' , $chart->accno.' has right balance') if $chart->accno eq '1600';
-      is($balance ,   '38.00000' , $chart->accno.' has right balance') if $chart->accno eq '1776';
-      is($balance , '-100.00000' , $chart->accno.' has right balance') if $chart->accno eq '3400';
-      is($balance ,  '200.00000' , $chart->accno.' has right balance') if $chart->accno eq '8400';
-      $::form->{"multi_id_${idx}"} = $chart->id;
-      $idx++ ;
-    }
-  }
-  $::form->{"rowcount"} = $idx-1;
-  #print "rowcount=". $::form->{"rowcount"}."\n";
-  $::form->{"login"}="unittests";
-
-  $yt_controller->make_booking;
-
-  ## no check cb ob booking :
-
-  my $sum_cb_p = 0;
-  my $sum_cb_m = 0;
-  foreach my $acc ( @{ SL::DB::Manager::AccTransaction->get_all(where => [ chart_id => $saldo_chart->id, cb_transaction => 't' ]) }) {
-    #print "cb amount=".$acc->amount."\n";
-    $sum_cb_p +=  $acc->amount if $acc->amount > 0;
-    $sum_cb_m += -$acc->amount if $acc->amount < 0;
-  }
-  #print "chart_id=".$saldo_chart->id." sum_cb_p=".$sum_cb_p." sum_cb_m=".$sum_cb_m."\n";
-  is($sum_cb_p ,  '357' , 'chart '.$saldo_chart->accno.' has right positive close saldo');
-  is($sum_cb_m ,  '357' , 'chart '.$saldo_chart->accno.' has right negative close saldo');
-  my $sum_ob_p = 0;
-  my $sum_ob_m = 0;
-  foreach my $acc ( @{ SL::DB::Manager::AccTransaction->get_all(where => [ chart_id => $saldo_chart->id, ob_transaction => 't' ]) }) {
-    #print "ob amount=".$acc->amount."\n";
-    $sum_ob_p +=  $acc->amount if $acc->amount > 0;
-    $sum_ob_m += -$acc->amount if $acc->amount < 0;
-  }
-  #print "chart_id=".$saldo_chart->id." sum_ob_p=".$sum_ob_p." sum_ob_m=".$sum_ob_m."\n";
-  is($sum_ob_p ,  '357' , 'chart '.$saldo_chart->accno.' has right positive open saldo');
-  is($sum_ob_m ,  '357' , 'chart '.$saldo_chart->accno.' has right negative open saldo');
-}
-
-
-
-1;
index 670b5ab..6c902c3 100644 (file)
@@ -26,11 +26,16 @@ 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();
@@ -96,6 +101,7 @@ sub test_import {
 
 ##### 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');
@@ -109,7 +115,7 @@ my $ar = SL::DB::Invoice->new(
   currency_id  => $currency_id,
   taxincluded  => 'f',
   customer_id  => $customer->id,
-  transdate    => DateTime->today,
+  transdate    => $transdate,
   employee_id  => SL::DB::Manager::Employee->current->id,
   transactions => [],
 );
@@ -138,7 +144,7 @@ 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     => DateTime->now->to_kivitendo,
+                   transdate     => $transdate,
                    payment_type  => 'without_skonto',  # default if not specified
                   );
 $result = $ar->validate_acc_trans(debug => 0);
@@ -152,10 +158,10 @@ my ($entries, $entry, $file);
 # 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
+$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
+"Rechnung",960,4,1,"invoice 1",f,1400,"$transdate_string"
 "AccTransaction",8400,159.48,3
 EOL
 $entries = test_import($file);
@@ -172,10 +178,10 @@ 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
+$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
+"Rechnung",960,4,1,"invoice 1",f,1400,"$transdate_string"
 "AccTransaction",8400,159.48,3
 EOL
 $entries = test_import($file);
@@ -184,10 +190,10 @@ $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
+$file = \<<"EOL";
+datatype,customer_id,taxzone_id,currency_id,taxincluded,archart,transdate
 datatype,accno,amount,taxkey
-"Rechnung",960,4,1,f,1400
+"Rechnung",960,4,1,f,1400,"$transdate_string"
 "AccTransaction",8400,159.48,3
 EOL
 $entries = test_import($file);
@@ -196,10 +202,10 @@ $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
+$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
+"Rechnung",960,4,1,"invoice 1 no amounts",f,1400,"$transdate_string"
 "AccTransaction",8400,159.48,3
 EOL
 $entries = test_import($file);
@@ -214,10 +220,10 @@ is $::form->round_amount($entry->{object}->transactions->[0]->amount, 2), '159.4
 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
+$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
+"Rechnung",960,4,1,"credit note",f,1400,"$transdate_string"
 "AccTransaction",8400,-159.48,3
 EOL
 $entries = test_import($file);
@@ -233,10 +239,10 @@ is $entry->{object}->amount, '-189.78', 'credit note amount tax not included is
 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
+$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
+"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);
@@ -244,10 +250,10 @@ $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
+$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
+"Rechnung",960,4,1,"invoice with direct debit",f,t,1400,"$transdate_string"
 "AccTransaction",8400,159.48,3
 EOL
 
@@ -257,10 +263,10 @@ $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
+$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
+"Rechnung",960,4,1,"invoice 1 tax included no amounts",t,1400,"$transdate_string"
 "AccTransaction",8400,189.78,3
 EOL
 
@@ -273,10 +279,10 @@ is $::form->round_amount($entry->{object}->netamount, 2), '159.48', 'taxincluded
 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
+$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
+"Rechnung",960,4,1,"invoice multiple tax included",t,1400,"$transdate_string"
 "AccTransaction",8400,94.89,3
 "AccTransaction",8400,94.89,3
 EOL
index 77a800e..6788958 100644 (file)
@@ -23,11 +23,13 @@ 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 $customer        = new_customer(name => 'Test customer', ustid => 'DE12345678')->save();
+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',
@@ -50,7 +52,7 @@ my $invoice = create_sales_invoice(
 );
 
 # lets make a boom
-# generate_datev_* doesnt care about encoding but
+# generate_datev_* doesn't care about encoding but
 # csv_buchungsexport does! all arabic will be deleted
 # and no string will be left as invnumber
 
@@ -60,10 +62,9 @@ my $datev1 = SL::DATEV->new(
 );
 
 my $startdate = DateTime->new(year => 2017, month =>  1, day =>  1);
-my $enddate   = DateTime->new(year => 2017, month =>  12, day => 31);
+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);
 
@@ -121,6 +122,13 @@ ok($die_message2 =~ m/Falscher Feldwert 'ݗݘݰݶmuh' für Feld 'belegfeld1' bei
 $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,
@@ -130,6 +138,7 @@ $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,
@@ -146,91 +155,90 @@ eval {
 ok(!($die_message3), 'no die message');
 ok(scalar @{ $datev_csv4->warnings } == 0, 'no warnings');
 
-my @sorted =  sort { $a->[0] cmp $b->[0] } @{ $lines_aref };
-cmp_deeply $sorted[0],    [ '1963,5', 'S', 'EUR', '', '', '',
-                            '1400', '8400', '', '1907', 'meine muh',
-                            '', '', 'Test customer', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', "K\x{e4}stchen",
-                            '299', '','DE12345678', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '',
-                          ];
-cmp_deeply $sorted[1],     [ '535', 'S', 'EUR', '', '', '',
-                             '1400', '8300', '', '1907','meine muh',
-                            '', '', 'Test customer', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', "K\x{e4}stchen",
-                            '299', '','DE12345678', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '', '', '', '', '', '', '', '', '',
-                            '',
-                          ];
+
+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
-my $tax_chart     = SL::DB::Manager::Chart->find_by(accno => '1576'); # Vorsteuer
-my $tax_9         = SL::DB::Manager::Tax->find_by(taxkey => 9, rate => 0.19) || die "No tax";
-
-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(
+
+note('testing gl transaction without deliverydate');
+my $gl_transaction = create_gl_transaction(
   reference      => "Reise März 2018",
-  description    => "Reisekonsten März 2018 / Ma Schmidt",
+  description    => "Reisekosten März 2018 / Ma Schmidt",
   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;
+  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,
@@ -247,17 +255,132 @@ my $datev_csv3  = SL::DATEV::CSV->new(datev_lines  => $datev2->generate_datev_li
                                      );
 
 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',
-                     '', '', 'Reisekonsten März 2018 / Ma Schmidt', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '' ]
-       );
+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
index 3625f88..3b2d4af 100644 (file)
@@ -73,6 +73,7 @@ cmp_deeply \@data_datev, [
                                            'konto'        => '1400',
                                            'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
                                            'kost2'        => 'Crowd-Funding September 2017',
+                                           'locked'       => undef,
                                            'umsatz'       => '249.9',
                                            'waehrung'     => 'EUR',
                                          },
@@ -84,14 +85,13 @@ cmp_deeply \@data_datev, [
                                            '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',
                                            'buchungstext' => 'Testcustomer',
                                            'datum'        => '05.01.2017',
                                            'gegenkonto'   => '1400',
@@ -99,6 +99,7 @@ cmp_deeply \@data_datev, [
                                            'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
                                            'kost2'        => 'Crowd-Funding September 2017',
                                            'umsatz'       => '784.9',
+                                           'locked'       => undef,
                                            'waehrung'     => 'EUR',
                                          },
                                        ], "trans_id datev check ok";
@@ -116,6 +117,7 @@ cmp_bag $datev1->generate_datev_lines, [
                                            'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
                                            'kost2'        => 'Crowd-Funding September 2017',
                                            'umsatz'       => '249.9',
+                                           'locked'       => undef,
                                            'waehrung'     => 'EUR',
                                          },
                                          {
@@ -127,6 +129,7 @@ cmp_bag $datev1->generate_datev_lines, [
                                            'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
                                            'kost2'        => 'Crowd-Funding September 2017',
                                            'umsatz'       => 535,
+                                           'locked'       => undef,
                                            'waehrung'     => 'EUR',
                                          },
                                          {
@@ -138,6 +141,7 @@ cmp_bag $datev1->generate_datev_lines, [
                                            'kost1'        => 'Kostenstelle DATEV-Schnittstelle 2018',
                                            'kost2'        => 'Crowd-Funding September 2017',
                                            'umsatz'       => '784.9',
+                                           'locked'       => undef,
                                            'waehrung'     => 'EUR',
                                          },
                                        ], "trans_id datev check use_pk ok";
@@ -177,7 +181,7 @@ cmp_deeply($data_csv[1], [ '535', 'S', 'EUR', '', '', '', '1400', '8300', '', '0
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '' ]
+                     '', '', '1', '', '', '', '', '', '' ]
        );
 
 cmp_deeply($data_csv[0], [ '249,9', 'S', 'EUR', '', '', '', '1400', '8400', '', '0101', "\x{de} sales \x{a5}& i",
@@ -189,7 +193,7 @@ cmp_deeply($data_csv[0], [ '249,9', 'S', 'EUR', '', '', '', '1400', '8400', '',
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '' ]
+                     '', '', '1', '', '', '', '', '', '' ]
        );
 cmp_deeply($data_csv[2], [ '784,9', 'S', 'EUR', '', '', '', '1200', '1400', '', '0501', "\x{de} sales \x{a5}& i",
                      '', '', 'Testcustomer', '', '', '', '', '', '', '', '',
@@ -200,7 +204,7 @@ cmp_deeply($data_csv[2], [ '784,9', 'S', 'EUR', '', '', '', '1200', '1400', '',
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
                      '', '', '', '', '', '', '', '', '', '', '', '', '',
-                     '', '', '', '', '' ]
+                     '', '', '1', '', '', '', '', '', '' ]
         );
 my $march_9 = DateTime->new(year => 2017, month =>  3, day => 9);
 my $invoice2 = create_sales_invoice(
@@ -243,7 +247,27 @@ $datev->generate_datev_data(use_pk => 1, from_to => $datev->fromto);
 $datev_lines = $datev->generate_datev_lines;
 
 note('testing purchase invoice');
-my $purchase_invoice = new_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,
@@ -260,6 +284,7 @@ cmp_deeply $datev1->generate_datev_lines, [
                                           'kost1'                  => undef,
                                           'kost2'                  => undef,
                                           'umsatz'                 => 119,
+                                          'locked'                 => undef,
                                           'waehrung'               => 'EUR'
                                         },
                                         {
@@ -271,6 +296,7 @@ cmp_deeply $datev1->generate_datev_lines, [
                                           'kost1'                  => undef,
                                           'kost2'                  => undef,
                                           'umsatz'                 => 107,
+                                          'locked'                 => undef,
                                           'waehrung'               => 'EUR'
                                         }
                                        ], "trans_id datev check purchase_invoice ok";
@@ -286,6 +312,7 @@ cmp_deeply $datev1->generate_datev_lines, [
                                           'kost1'                  => undef,
                                           'kost2'                  => undef,
                                           'umsatz'                 => 119,
+                                          'locked'                 => undef,
                                           'waehrung'               => 'EUR'
                                         },
                                         {
@@ -297,11 +324,14 @@ cmp_deeply $datev1->generate_datev_lines, [
                                           '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,
@@ -323,108 +353,6 @@ cmp_deeply $datev->generate_datev_lines, [], "no bookings for January made after
 done_testing();
 clear_up();
 
-sub new_purchase_invoice {
-  # 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
-  # this function was essentially copied from t/db_helper/payment.t, refactor once $purchase_invoice->post exists
-  my $currency_id = $::instance_conf->get_currency_id;
-  my $employee    = SL::DB::Manager::Employee->current                          || die "No employee";
-  my $taxzone     = SL::DB::Manager::TaxZone->find_by( description => 'Inland') || die "No taxzone";
-
-  my $purchase_invoice = SL::DB::PurchaseInvoice->new(
-    amount      => '226',
-    currency_id => $currency_id,
-    employee_id => $employee->id,
-    gldate      => $date,
-    invnumber   => "ap1",
-    invoice     => 0,
-    itime       => $date,
-    mtime       => $date,
-    netamount   => '200',
-    paid        => '0',
-    taxincluded => 0,
-    taxzone_id  => $taxzone->id,
-    transdate   => $date,
-    type        => 'invoice',
-    vendor_id   => $vendor->id,
-  )->save;
-
-  my $expense_chart  = SL::DB::Manager::Chart->find_by(accno => '3400');
-  my $expense_chart_booking= SL::DB::AccTransaction->new(
-    amount     => '-100',
-    chart_id   => $expense_chart->id,
-    chart_link => $expense_chart->link,
-    itime      => $date,
-    mtime      => $date,
-    source     => '',
-    tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 9)->id,
-    taxkey     => 9,
-    transdate  => $date,
-    trans_id   => $purchase_invoice->id,
-  );
-  $expense_chart_booking->save;
-
-  my $tax_chart  = SL::DB::Manager::Chart->find_by(accno => '1576');
-  my $tax_chart_booking= SL::DB::AccTransaction->new(
-    amount     => '-19',
-    chart_id   => $tax_chart->id,
-    chart_link => $tax_chart->link,
-    itime      => $date,
-    mtime      => $date,
-    source     => '',
-    tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 9)->id,
-    taxkey     => 0,
-    transdate  => $date,
-    trans_id   => $purchase_invoice->id,
-  );
-  $tax_chart_booking->save;
-  $expense_chart  = SL::DB::Manager::Chart->find_by(accno => '3300');
-  $expense_chart_booking= SL::DB::AccTransaction->new(
-    amount     => '-100',
-    chart_id   => $expense_chart->id,
-    chart_link => $expense_chart->link,
-    itime      => $date,
-    mtime      => $date,
-    source     => '',
-    tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 8)->id,
-    taxkey     => 8,
-    transdate  => $date,
-    trans_id   => $purchase_invoice->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  => $date,
-    itime      => $date,
-    mtime      => $date,
-    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  => $date,
-    itime      => $date,
-    mtime      => $date,
-    source     => '',
-    taxkey     => 0,
-    tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id,
-  );
-  $arap_booking->save;
-
-  return $purchase_invoice;
-}
-
 sub clear_up {
   SL::DB::Manager::AccTransaction->delete_all(all => 1);
   SL::DB::Manager::InvoiceItem->delete_all(   all => 1);
index 5622961..8d08899 100644 (file)
@@ -25,6 +25,7 @@ use SL::DB::Unit;
 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
 
@@ -40,6 +41,9 @@ sub reset_state {
 
   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
+
   $unit     = SL::DB::Manager::Unit->find_by(name => 'kg') || die "Can't find unit 'kg'";
   $customer = new_customer()->save;
 
@@ -91,6 +95,7 @@ reset_state();
 # we create L20199 with two items
 my $do1 = create_sales_delivery_order(
   'department_id' => $department->id,
+  'transdate'     => $transdate,
   'donumber'      => 'L20199',
   'employee_id'   => $employee->id,
   'intnotes'      => 'some intnotes',
@@ -144,7 +149,7 @@ is (SL::DB::Manager::DeliveryOrderItem->get_all_count(where => [ delivery_order_
 
 
 # convert this do to invoice
-my $invoice = $do1->convert_to_invoice();
+my $invoice = $do1->convert_to_invoice(transdate => $transdate);
 
 sleep (300) if $VISUAL_TEST; # we can do a real visual test via gui login
 # test invoice afterwards
@@ -234,7 +239,6 @@ clear_up();
 
 1;
 
-
 # vim: ft=perl
 # set emacs to perl mode
 # Local Variables:
index b23958e..89610d6 100644 (file)
@@ -11,7 +11,7 @@ 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);
+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::Buchungsgruppe;
@@ -33,7 +33,7 @@ my ($customer, $vendor, $currency_id, @parts, $buchungsgruppe, $buchungsgruppe7,
 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 $purchase_invoice_counter = 0; # used for generating purchase invnumber
+my $ap_transaction_counter = 0; # used for generating purchase invnumber
 
 Support::TestSetup::login();
 
@@ -45,8 +45,8 @@ 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_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();
 
@@ -56,13 +56,13 @@ test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_final_d
 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();
+test_default_ap_transaction_two_charts_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto();
 
 # 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_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();
 
@@ -110,10 +110,11 @@ sub init_state {
 
   clear_up();
 
-  $transdate1 = DateTime->today;
-  $transdate2 = DateTime->today->add(days => 1);
-  $transdate3 = DateTime->today->add(days => 2);
-  $transdate4 = DateTime->today->add(days => 3);
+  $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\%";
@@ -238,88 +239,31 @@ sub init_state {
   $ap_amount_chart = SL::DB::Manager::Chart->find_by( accno => '3400' ); # Wareneingang 19%
 }
 
-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      => $transdate1,
-    taxzone_id  => $taxzone->id,
-    transdate   => $transdate1,
-    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 $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  => $transdate1,
-                                        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  => $transdate1,
-                                        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  => $transdate1,
-                                        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  => $transdate1,
-                                         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  => $transdate1,
-                                                source     => '',
-                                                taxkey     => 0,
-                                                tax_id     => SL::DB::Manager::Tax->find_by(taxkey => 0)->id);
-  $arap_booking->save;
-
-  return $purchase_invoice;
+    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 {
@@ -331,7 +275,7 @@ sub number_of_payments {
     if ( $transaction->chart_link =~ /(AR_paid|AP_paid)/ ) {
       $paid_amount += $transaction->amount ;
       $number_of_payments++;
-    };
+    }
   };
   return ($number_of_payments, $paid_amount);
 };
@@ -351,16 +295,17 @@ 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,
   );
 
-  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';
@@ -386,16 +331,17 @@ sub test_default_invoice_one_item_19_without_skonto_overpaid {
   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,
   );
 
-  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';
@@ -426,13 +372,14 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto {
   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,
   );
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{payment_type} = 'with_skonto_pt';
@@ -457,6 +404,7 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto_tax_included {
   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,
@@ -464,7 +412,7 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto_tax_included {
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{payment_type} = 'with_skonto_pt';
@@ -494,6 +442,7 @@ sub test_default_invoice_two_items_19_7_without_skonto {
   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,
@@ -501,7 +450,7 @@ sub test_default_invoice_two_items_19_7_without_skonto {
 
   # 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
@@ -528,6 +477,7 @@ sub test_default_invoice_two_items_19_7_without_skonto_incomplete_payment {
   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,
   );
@@ -535,7 +485,7 @@ sub test_default_invoice_two_items_19_7_without_skonto_incomplete_payment {
   $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);
@@ -556,6 +506,7 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments {
   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,
@@ -564,11 +515,11 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments {
   $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);
@@ -591,6 +542,7 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fin
   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,
   );
@@ -598,17 +550,17 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fin
   $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
+                         transdate    => $transdate1,
                        );
 
   my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
@@ -635,6 +587,7 @@ sub  test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fi
   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,
   );
@@ -642,12 +595,12 @@ sub  test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fi
   $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',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
 
   my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
@@ -670,6 +623,7 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fin
   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,
   );
@@ -677,12 +631,12 @@ sub test_default_invoice_two_items_19_7_tax_without_skonto_multiple_payments_fin
   $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',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
 
   my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
@@ -703,13 +657,14 @@ sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto
   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,
   );
 
   # default values
   my %params = ( chart_id  => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount}       = '2.32';
@@ -742,13 +697,14 @@ sub test_default_invoice_one_item_19_multiple_payment_final_difference_as_skonto
   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,
   );
 
   # default values
   my %params = ( chart_id  => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount}       = '6.95';
@@ -772,22 +728,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 {
+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 );
+  $ap_transaction->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice);
-  my $total = total_amount($purchase_invoice);
+  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");
@@ -795,22 +751,23 @@ sub test_default_purchase_invoice_two_charts_19_7_without_skonto {
 
 }
 
-sub test_default_purchase_invoice_two_charts_19_7_with_skonto {
+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
+                 transdate => $transdate1,
                );
 
   # $params{amount} = '226'; # pass full amount
   $params{payment_type} = 'with_skonto_pt';
 
-  $purchase_invoice->pay_invoice( %params );
+  $ap_transaction->payment_terms($ap_transaction->vendor->payment);
+  $ap_transaction->pay_invoice( %params );
 
-  my ($number_of_payments, $paid_amount) = number_of_payments($purchase_invoice);
-  my $total = total_amount($purchase_invoice);
+  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");
@@ -818,19 +775,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 {
-  my $title = 'default purchase_invoice, two charts, 19/7% tax multiple payments with final difference as 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
-  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);
-  my $total = total_amount($purchase_invoice);
+  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");
@@ -838,32 +795,32 @@ 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 {
-  my $title = 'default purchase_invoice, two charts, 19/7% tax multiple payments with final difference as skonto';
+sub test_default_ap_transaction_two_charts_19_7_tax_without_skonto_multiple_payments_final_difference_as_skonto {
+  my $title = 'default ap_transaction, two charts, 19/7% tax multiple payments with final difference as 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(
+  $ap_transaction->pay_invoice(
                           payment_type => 'difference_as_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);
-  my $total = total_amount($purchase_invoice);
+  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");
@@ -879,13 +836,14 @@ sub test_default_invoice_two_items_19_7_tax_with_skonto_50_50 {
   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,
   );
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount} = $invoice->amount_less_skonto;
@@ -914,13 +872,14 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25 {
   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,
   );
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount} = $invoice->amount_less_skonto;
@@ -948,13 +907,14 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_tax_included {
   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,
   );
 
   # default values
   my %params = ( chart_id => $bank_account->chart_id,
-                 transdate => DateTime->today_local->to_kivitendo
+                 transdate => $transdate1,
                );
 
   $params{amount} = $invoice->amount_less_skonto;
@@ -985,6 +945,7 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple {
   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,
   );
@@ -992,11 +953,11 @@ sub test_default_invoice_four_items_19_7_tax_with_skonto_4x_25_multiple {
   $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',
                          chart_id     => $bank_account->chart_id,
-                         transdate    => DateTime->today_local->to_kivitendo
+                         transdate    => $transdate1,
                        );
 
   my ($number_of_payments, $paid_amount) = number_of_payments($invoice);
@@ -1441,13 +1402,14 @@ sub test_credit_note_two_items_19_7_tax_tax_not_included {
   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 => DateTime->today_local->to_kivitendo,
+                 transdate => $transdate1,
                );
 
   $params{amount}       = $invoice->amount,
index 6caae1d..5050ecd 100644 (file)
@@ -24,6 +24,7 @@ use SL::DB::Unit;
 use SL::DB::TaxZone;
 
 my ($customer, @parts, $buchungsgruppe, $buchungsgruppe7, $unit, $employee, $tax, $tax7, $taxzone);
+my ($transdate);
 
 sub clear_up {
   SL::DB::Manager::Order->delete_all(all => 1);
@@ -92,6 +93,7 @@ sub new_invoice {
   my %params  = @_;
 
   return create_sales_invoice(
+    transdate   => $transdate,
     taxzone_id  => $taxzone->id,
     %params,
   );
@@ -117,7 +119,7 @@ sub test_default_invoice_one_item_19_tax_not_included() {
     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
@@ -153,9 +155,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,
@@ -178,8 +183,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
@@ -193,6 +198,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
@@ -234,10 +240,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,
@@ -267,7 +277,7 @@ 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);
+  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
@@ -338,9 +348,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,
@@ -374,7 +387,7 @@ sub test_default_invoice_one_item_19_tax_not_included_rounding_discount() {
     invoiceitems => [ $item ],
   );
 
-  my %taxkeys = map { ($_->id => $_->get_taxkey(date => DateTime->today_local, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($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
   #
@@ -406,9 +419,12 @@ sub test_default_invoice_one_item_19_tax_not_included_rounding_discount() {
       [],
     ],
     exchangerate                                 => 1,
-    taxes                                        => {
+    taxes_by_chart_id                            => {
       $tax->chart_id                             => 0.66,
     },
+    taxes_by_tax_id                              => {
+      $tax->id                                   => 0.66310,
+    },
     items                                        => [
       { linetotal                                => 3.49,
         linetotal_cost                           => 0,
@@ -430,7 +446,7 @@ sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_huge_qty
     invoiceitems => [ $item ],
   );
 
-  my %taxkeys = map { ($_->id => $_->get_taxkey(date => DateTime->today_local, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($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;
@@ -456,9 +472,12 @@ sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_huge_qty
       [],
     ],
     exchangerate                                 => 1,
-    taxes                                        => {
+    taxes_by_chart_id                            => {
       $tax->chart_id                             => 1843,
     },
+    taxes_by_tax_id                              => {
+      $tax->id                                   => 1843,
+    },
     items                                        => [
       { linetotal                                => 9700,
         linetotal_cost                           => 0,
@@ -480,7 +499,7 @@ sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_big_qty_
     invoiceitems => [ $item ],
   );
 
-  my %taxkeys = map { ($_->id => $_->get_taxkey(date => DateTime->today_local, is_sales => 1, taxzone => $invoice->taxzone_id)) } uniq map { $_->part } ($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
@@ -517,9 +536,12 @@ sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_big_qty_
       [],
     ],
     exchangerate                                 => 1,
-    taxes                                        => {
+    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,
@@ -535,6 +557,9 @@ sub test_default_invoice_one_item_19_tax_not_included_rounding_discount_big_qty_
 
 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();
diff --git a/t/gl/gl.t b/t/gl/gl.t
new file mode 100644 (file)
index 0000000..8cc0f6a
--- /dev/null
+++ b/t/gl/gl.t
@@ -0,0 +1,284 @@
+use strict;
+use Test::More tests => 4;
+
+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"
+         );
+
+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 762c639..f8ea56e 100644 (file)
@@ -16,12 +16,16 @@ 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;
@@ -35,13 +39,13 @@ sub reset_state {
                           )->save;
 }
 
-sub save_shorcontroller_to_string {
+sub save_shopcontroller_to_string {
 
   my $output;
   open(my $outputFH, '>', \$output) or die "OUTPUT";
   my $oldFH = select $outputFH;
-  my $shor_controller = SL::Controller::ShopOrder->new;
-  $shor_controller->action_transfer;
+  my $shop_controller = SL::Controller::ShopOrder->new;
+  $shop_controller->action_transfer;
 
   select $oldFH;
   close $outputFH;
@@ -53,7 +57,7 @@ sub test_transfer {
   $::form->{import_id} = $params{import_id};
   $::form->{customer} =  $params{customer};
   my $test_name = 'Test Controller Action Transfer';
-  save_shorcontroller_to_string();
+  save_shopcontroller_to_string();
   my @links_record = RecordLinks->get_links( 'from_table' => 'shop_orders',
                                             'from_id'    => $params{import_id},
                                             'to_table'   => 'oe',
@@ -73,7 +77,9 @@ 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',
@@ -141,7 +147,7 @@ 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);
+my $order = $shop_order->convert_to_sales_order(employee => $employee, customer => $customer, transdate => $shop_order->order_date);
 $order->calculate_prices_and_taxes;
 $order->save;
 
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");
   }
 }
 
diff --git a/t/tax/tax.t b/t/tax/tax.t
new file mode 100644 (file)
index 0000000..e2861da
--- /dev/null
@@ -0,0 +1,600 @@
+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::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);
+
+# 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',
+             );
+
+$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::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;
diff --git a/t/year_end/year_end.t b/t/year_end/year_end.t
new file mode 100644 (file)
index 0000000..5976edd
--- /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 if DateTime->today_local->year == 2020;
+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 ac83cce..b34ce11 100644 (file)
@@ -1,7 +1,7 @@
 kivitendo selftest report.
 
 [% IF errors %]
-  General error(s) have occured.
+  General error(s) have occurred.
   [% errors %]
 [% END %]
 
@@ -15,7 +15,7 @@ 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/pdf/pdf_a_metadata.xmp b/templates/pdf/pdf_a_metadata.xmp
new file mode 100644 (file)
index 0000000..a31dfdb
--- /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>ZUGFeRD PDFA Extension Schema</pdfaSchema:schema>
+      <pdfaSchema:namespaceURI>urn:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#</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 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 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:zugferd:pdfa:CrossIndustryDocument:invoice:2p0#"
+                   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 56e4067..7a60d3d 100644 (file)
 
 \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 85bb98c..d838430 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}
index 4cbf253..326d041 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}
index f3a7761..1435e73 100644 (file)
 
 \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%>
index 745047e..c114e5c 100644 (file)
 
 \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 8e2e17a..97cb004 100644 (file)
 
 \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 77818e8..80b426b 100644 (file)
 
 \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 39ad90b..fcda41a 100644 (file)
           <%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%>
           <%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]
index 1949fe5..ad0de3e 100644 (file)
 
 \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 customer_make%>
+            <%foreach customer_make%>
+              \ifthenelse{\equal{<%customer_make%>}{<%name%>}}{&& \kundenartnr: <%customer_model%>\\}{}
+            <%end foreach%>
+          <%end if%>
           \\[-0.8em]
 <%end number%>
 
index 90df785..4010a7f 100644 (file)
 
 \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 customer_make%>
+            <%foreach customer_make%>
+              \ifthenelse{\equal{<%customer_make%>}{<%name%>}}{&& \kundenartnr: <%customer_model%>\\}{}
+            <%end foreach%>
+          <%end if%>
           \\[-0.8em]
 <%end number%>
 
index 1feb566..7435a17 100644 (file)
 
 \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 cf8680e..f125de9 100644 (file)
 
 \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 473b3b9..b96dd96 100644 (file)
 
 \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 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..d2d8044
--- /dev/null
@@ -0,0 +1,73 @@
+
+# Bemerkungen zum Vorlagensatz von
+### © 2020 by Marei Peischl (peiTeX TeXnical Solutions)
+
+## 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 im
+    unteren Abschnitt der Datei 'insettings.tex', Kommentar Fußzeile
+    (cfoot) eingefügt werden.
+   Briefbogen/Logos:
+    Eine Hintergrundgrafik oder ein Logo kann in Abhängigkeit vom
+    Medium (z.B. nur beim Verschicken mit E-Mail) eingebunden
+    werden. Dies ist im Moment auskommentiert.
+    
+    Desweiteren sind (auskommentierte) Beispiele enthalten für eine
+    Grafik als Briefkopf, nur ein Logo, oder ein komplettes DinA4-PDF
+    als Briefpapier.
+    
+    Fusszeile:
+    Die Tabelle im Fuß verwendet die Angaben aus firma/ident.tex und
+    firma/*_account.tex.
+        
+## Tabellen:
+
+
+ Quickstart (wo kann was angepasst werden?):
+    insettings.tex : Pfad zu Angaben über Mandanten (default: firma)
+                     Logo/Briefpapier
+                     Layout der Kopf/Fußzeile
+    firma/*        : Angaben über Mandanten
+ 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
+
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..83c899b
--- /dev/null
@@ -0,0 +1,113 @@
+\documentclass[twoside]{scrartcl}
+\usepackage[frame]{xy}
+\usepackage{tabularx}
+\usepackage[utf8]{inputenc}
+\usepackage{graphicx}
+\setlength{\voffset}{0.5cm}
+\setlength{\hoffset}{-2.0cm}
+\setlength{\topmargin}{0cm}
+\setlength{\headheight}{0.5cm}
+\setlength{\headsep}{1cm}
+\setlength{\topskip}{0pt}
+\setlength{\oddsidemargin}{1.0cm}
+\setlength{\evensidemargin}{1.0cm}
+\setlength{\textwidth}{17cm}
+\setlength{\textheight}{24.7cm}
+\setlength{\footskip}{1cm}
+\setlength{\parindent}{0pt}
+\renewcommand{\baselinestretch}{1}
+
+\begin{document}
+
+\pagestyle{myheadings}
+\thispagestyle{empty}
+
+\fontfamily{cmss}\fontsize{10pt}{12pt}\selectfont
+
+\vspace*{-1.3cm}
+
+\parbox{\textwidth}{
+  \parbox[b]{.42\textwidth}{%
+    <%company%>
+
+    <%address%>
+  }\hfill
+  \begin{tabular}[b]{rr@{}}
+  Tel & <%tel%>\\
+  Fax & <%fax%>
+  \end{tabular}
+
+  \rule[1.5ex]{\textwidth}{0.5pt}
+}
+
+\vspace*{0.5cm}
+
+\parbox[t]{1cm}{\hfill}
+\parbox[t]{.5\textwidth}{
+\textbf{Von}
+\vspace{0.7cm}
+
+<%name%> \\
+<%street%> \\
+<%zipcode%> \\
+<%city%> \\
+<%country%>
+}
+\parbox[t]{.4\textwidth}{
+\textbf{Lieferanschrift}
+\vspace{0.7cm}
+
+<%shiptoname%> \\
+<%shiptostreet%> \\
+<%shiptozipcode%> \\
+<%shiptocity%> \\
+<%shiptocountry%>
+}
+\hfill
+
+\vspace{1cm}
+
+\textbf{L A G E R L I S T E}
+\hfill
+
+\vspace{1cm}
+
+\begin{tabularx}{\textwidth}{*{6}{|X}|} \hline
+  \textbf{BestellNr. \#} & \textbf{Datum} & \textbf{Kontakt}
+  <%if warehouse%>
+  & \textbf{Lager}
+  <%end warehouse%>
+  & \textbf{Lagerplatz} & \textbf{Lieferung mit} \\ [0.5em]
+  \hline
+
+  <%ordnumber%>
+  <%if shippingdate%>
+  & <%shippingdate%>
+  <%end shippingdate%>
+  <%if not shippingdate%>
+  & <%orddate%>
+  <%end shippingdate%>
+  & <%employee%>
+  <%if warehouse%>
+  & <%warehouse%>
+  <%end warehouse%>
+  & <%shippingpoint%> & <%shipvia%> \\
+  \hline
+\end{tabularx}
+
+\vspace{1cm}
+
+\begin{tabularx}{\textwidth}{@{}rlXllrrll@{}}
+  \textbf{Pos} & \textbf{Nummer} & \textbf{Beschreibung} & \textbf{Seriennumner} & & \textbf{Menge} & \textbf{Erh} & & \textbf{Lagerplatz} \\
+
+<%foreach number%>
+  <%runningnumber%> & <%number%> & <%description%> & <%serialnumber%> &
+  <%deliverydate%> & <%qty%> & <%ship%> & <%unit%> & <%bin%> \\
+<%end number%>
+\end{tabularx}
+
+
+\rule{\textwidth}{2pt}
+
+\end{document}
+
diff --git a/templates/print/marei/check.tex b/templates/print/marei/check.tex
new file mode 100644 (file)
index 0000000..6086d45
--- /dev/null
@@ -0,0 +1,71 @@
+\documentclass[twoside]{scrartcl}
+\usepackage[frame]{xy}
+\usepackage{tabularx}
+\usepackage[utf8]{inputenc}
+\setlength{\voffset}{0.4cm}
+\setlength{\hoffset}{-2.0cm}
+\setlength{\topmargin}{0cm}
+\setlength{\headheight}{0.0cm}
+\setlength{\headsep}{1cm}
+\setlength{\topskip}{0pt}
+\setlength{\oddsidemargin}{1.0cm}
+\setlength{\evensidemargin}{1.0cm}
+\setlength{\textwidth}{17cm}
+\setlength{\textheight}{24.5cm}
+\setlength{\footskip}{1cm}
+\setlength{\parindent}{0pt}
+\renewcommand{\baselinestretch}{1}
+\begin{document}
+
+
+\fontfamily{cmss}\fontsize{9pt}{9pt}\selectfont
+
+\parbox[t]{12cm}{
+  <%company%>
+
+  <%address%>}
+\hfill
+\parbox[t]{6cm}{\hfill <%source%>}
+
+\vspace*{0.6cm}
+
+<%text_amount%> \dotfill <%decimal%>/100 \makebox[0.5cm]{\hfill}
+
+\vspace{0.5cm}
+
+\hfill <%datepaid%> \makebox[2cm]{\hfill} <%amount%>
+
+\vspace{0.5cm}
+
+<%name%>
+
+<%street%>
+
+<%zipcode%>
+
+<%city%>
+
+<%country%>
+
+\vspace{2.8cm}
+
+<%company%>
+
+\vspace{0.5cm}
+
+<%name%> \hfill <%datepaid%> \hfill <%source%>
+
+\vspace{0.5cm}
+\begin{tabularx}{\textwidth}{lXrr@{}}
+\textbf{Rechnung} & \textbf{Ausgestellt}
+  & \textbf{Fällig} & \textbf{Verrechnet} \\
+<%foreach invnumber%>
+<%invnumber%> & <%invdate%> \dotfill
+  & <%due%> & <%paid%> \\
+<%end invnumber%>
+\end{tabularx}
+
+\vfill
+
+\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..ea84e00
--- /dev/null
@@ -0,0 +1,127 @@
+\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%>}
+
+
+\begin{document}
+       
+
+<%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%>
+
+       
+\setkomavar{title}{
+       \gutschrift~
+       \nr ~<%invnumber%>
+}
+
+<%if invnumber_for_credit_note%>
+\setkomavar*{myref}{\fuerRechnung}
+\setkomavar{myref}{<%invnumber_for_credit_note%>}
+<%end if%>
+
+\setkomavar*{date}{\datum}
+
+\setkomavar{date}{<%transdate%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+       
+\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
+
+\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 <%ordtotal%>\\
+       \end{PricingTotal}
+\end{PricingTabular*}
+
+<%if notes%>
+<%notes%>
+\medskip
+<%end if%>
+
+\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..eeb94fb
--- /dev/null
@@ -0,0 +1,138 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%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{\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{\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:}
+
+% 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}
+
+% Brief/letter
+\newcommand{\ihrzeichen}{Ihr Zeichen}
+\newcommand{\betreff}{Betreff}
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..13f2a9e
--- /dev/null
@@ -0,0 +1,137 @@
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%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{\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{\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:}
+
+% 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}
+
+% Brief/letter
+\newcommand{\ihrzeichen}{Your reference}
+\newcommand{\betreff}{Subject}
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/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..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}
diff --git a/templates/print/marei/firma/default_account.tex b/templates/print/marei/firma/default_account.tex
new file mode 100644 (file)
index 0000000..e436c3d
--- /dev/null
@@ -0,0 +1,6 @@
+\newcommand{\kontonummer}{4071953800}
+\newcommand{\bank}{GLS Bank eG}
+\newcommand{\bankleitzahl}{430 609 67}
+\newcommand{\bic}{DE50430609674071953800}
+\newcommand{\iban}{GENODEM1GLS}
+%Keine Definition von \currency!
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..4963272
--- /dev/null
@@ -0,0 +1,6 @@
+\newcommand{\currency}{€}
+\newcommand{\kontonummer}{4071953800}
+\newcommand{\bank}{GLS Bank eG}
+\newcommand{\bankleitzahl}{430 609 67}
+\newcommand{\bic}{DE50430609674071953800}
+\newcommand{\iban}{GENODEM1GLS}
diff --git a/templates/print/marei/firma/ident.tex b/templates/print/marei/firma/ident.tex
new file mode 100644 (file)
index 0000000..fbb6c45
--- /dev/null
@@ -0,0 +1,9 @@
+\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}
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..b2dd261
--- /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..6bda090
--- /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..8824d62
--- /dev/null
@@ -0,0 +1,118 @@
+%% insettings.tex
+%% Copyright 2019 Marei Peischl
+\ProvidesFile{insettings.tex}[2019/12/22 Konfigurationsdatei kivitendo ERP]
+% Sprachüberprüfung
+\RequirePackage[english, ngerman]{babel}
+\ifstr{\lxlangcode}{EN}{
+       \makeatletter
+       \main@language{english}
+       \makeatother
+       \input{english.tex}}{
+       \ifstr{\lxlangcode}{DE}{
+               \makeatletter
+               \main@language{ngerman}
+               \makeatother
+               \input{deutsch.tex}}{\input{deutsch.tex}}
+} % Ende EN
+
+
+% Mandanten-/Firmenabhängigkeiten
+
+% Pfad zu firmenspez. Angaben
+% Hat man mehrere Mandanten muß man statt "Firma1" den Datenbanknamen seines
+% Mandanten eingeben.
+
+\ExplSyntaxOn
+\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}
+       {
+       \str_if_in:NnTF \kivicompany {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}
+       }
+}
+
+\ExplSyntaxOff
+
+
+% Identität
+\input{\identpath/ident.tex}
+
+\ExplSyntaxOn
+
+%Setze Briefkopf-logo falls vorhanden
+\setkomavar{fromlogo}{\includegraphics[width=.25\linewidth]{\identpath/briefkopf}}
+
+% Währungen/Konten
+\tl_new:N \g_kivi_currency_tl
+\str_if_in:NnT \lxcurrency {USD} {\tl_gset:Nn \g_kivi_currency_tl {usd}}
+\str_if_in:NnT \lxcurrency {CHF} {\tl_gset:Nn \g_kivi_currency_tl {chf}}
+\str_if_in:NnT \lxcurrency {EUR} {\tl_gset:Nn \g_kivi_currency_tl {euro}}
+\tl_if_empty:NT  \g_kivi_currency_tl {
+       \tl_gset:Nn \g_kivi_currency_tl {default}
+       \edef \currency {\tl_to_str:N \lxcurrency}
+}
+
+\input{\identpath/\g_kivi_currency_tl _account.tex}
+
+\ExplSyntaxOff
+
+
+% keine Absätze nach rechts einrücken
+\setlength\parindent{0pt}
+
+
+
+% Befehl f. normale Schriftart und -größe
+\renewcommand*{\familydefault}{\sfdefault}
+\KOMAoptions{fontsize=10pt}
+
+% Einstellungen f. Kopf und Fuss
+\pagestyle{kivitendo.letter}
+% Befehl f. laufende Kopfzeile:
+% 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%>}
+\setkomafont{pagehead}{\scriptsize}
+\newcommand{\ourhead}[5] {
+\chead{
+  \ifnum\thepage=1
+  \else
+      \makebox[\textwidth]{
+      \ifstr{#1}{}{}{#1: #2 \hspace{0.7cm}}
+      #3
+      \ifstr{#4}{}{}{~\nr: #4}
+      \ifstr{#5}{}{}{\vom ~ #5}
+      \hspace{0.7cm} - \seite ~ \thepage/\letterlastpage  ~-%
+      }
+  \fi
+}
+}
+
+%% % Firmenfuss
+\setkomafont{pagefoot}{\tiny}
+\cfoot{
+  {
+     \begin{tabular*}{\textwidth}{@{\extracolsep{\fill}}p{5cm}p{4.5cm}lr@{}}
+        \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*}
+  }
+}
+
+\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..418fd00
--- /dev/null
@@ -0,0 +1,158 @@
+\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%>
+}
+<%if ordnumber%>
+\setkomavar*{myref}{\auftragsnummer}
+\setkomavar{myref}{<%ordnumber%>}
+<%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%>
+
+<%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}
+
+<%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 <%ordtotal%>\\
+\end{PricingTotal}
+\end{PricingTabular*}
+
+\vspace{0.2cm}
+
+\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/kiviletter.sty b/templates/print/marei/kiviletter.sty
new file mode 100644 (file)
index 0000000..fcc4967
--- /dev/null
@@ -0,0 +1,359 @@
+\NeedsTeXFormat{LaTeX2e}
+\ProvidesPackage{kiviletter}[2020/04/24 Letter Layouts for Kivitendo]
+
+
+\newif\if@kivi@infobox
+\DeclareOption{reffields}{\@kivi@infoboxfalse}
+\DeclareOption{infobox}{\@kivi@infoboxtrue}
+\@kivi@infoboxtrue
+
+\ProcessOptions\relax
+
+
+\RequirePackage{expl3}
+\RequirePackage{iftex}
+\KOMAoptions{fontsize=12pt}
+% Schriftart, Eingabelayout der Tastatur
+\ifPDFTeX
+\RequirePackage[utf8]{inputenc}% Nur notwendig, wenn Basis älter als TL2018
+\RequirePackage[T1]{fontenc}
+\RequirePackage{lmodern}
+\else
+\RequirePackage{fontspec}
+\fi
+
+%\RequirePackage{xltabular}
+\RequirePackage{tabularx}
+\RequirePackage{longtable}
+\RequirePackage{booktabs}
+\PassOptionsToPackage{table}{xcolor}
+
+\RequirePackage{xcolor}
+\RequirePackage{graphicx}
+
+\ifPDFTeX
+\RequirePackage{eurosym}
+\DeclareUnicodeCharacter{20AC}{\euro}
+\fi
+
+\RequirePackage[fromlogo,fromalign=right,
+  firstfoot=false,%Für einheitliche Randeinstellungen
+  refline=nodate,
+       ]{scrletter}
+\LoadLetterOption{DIN}
+
+\newkomavar{transaction}
+\newkomavar[\lieferschein{}~\nr]{delivery}
+\newkomavar[\angebot{}~\nr]{quote}
+\newkomavar{orderID}
+\newkomavar{projectID}
+
+\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}
+\savegeometry{kivi.letter@default}
+%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
+\int_new:N \g_PricingTabular_firstpage_int
+\ExplSyntaxOff
+
+\newsavebox{\shippingAddressBox}
+
+
+\DeclareNewLayer[
+foreground,
+hoffset=\useplength{toaddrhpos},
+voffset=\dimexpr\useplength{toaddrvpos}+\useplength{toaddrheight}+4\baselineskip,
+contents={\usebox\shippingAddressBox}
+]{kivitendo.shippingaddress}
+
+
+\ExplSyntaxOn
+\DeclareNewLayer[
+foreground,
+mode=picture,
+hoffset=\g_kivi_margin_dim,
+voffset=\g_kivi_margin_dim,
+align=tl,
+height=\box_ht:N \g_kivi_LT@head_box,
+contents={\box_use:N \g_kivi_LT@head_box},
+]{kivitendo.TableHead}
+
+
+\DeclareNewLayer[
+foreground,
+textarea,
+mode=picture,
+voffset=\dim_eval:n {\paperheight-\g_kivi_margin_dim},
+height=\box_ht:N \g_kivi_LT@foot_box,
+contents=\bool_if:NT \g_kivi_TableFoot_bool {\box_use:N \g_kivi_LT@foot_box},
+align=bl,
+]{kivitendo.TableFoot}
+
+\AtBeginLetter{\dim_gset:Nn \g_kivi_orig@textheight_dim {\textheight}}
+\ExplSyntaxOff
+
+\newpairofpagestyles{kivitendo.letter}{}
+\renewcommand*{\letterpagestyle}{kivitendo.letter}
+
+\DeclareNewPageStyleByLayers{kivitendo.letter.PricingTable}{
+       kivitendo.TableHead,
+       kivitendo.TableFoot
+       kivitendo.letter.head.odd,kivitendo.letter.head.even,kivitendo.letter.head.oneside,%
+       kivitendo.letter.foot.odd,kivitendo.letter.foot.even,kivitendo.letter.foot.oneside,%
+}
+\DeclareNewPageStyleByLayers{kivitendo.letter.first}{
+       kivitendo.shippingaddress,
+       kivitendo.TableFoot,
+       kivitendo.letter.head.odd,kivitendo.letter.head.even,kivitendo.letter.head.oneside,%
+       kivitendo.letter.foot.odd,kivitendo.letter.foot.even,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 \g_kivi_tab_pos_dim
+\dim_gset:Nn \g_kivi_tab_pos_dim {3.5ex}
+\dim_new:N \g_kivi_tab_id_dim
+\dim_gset:Nn \g_kivi_tab_id_dim {4em}
+\dim_new:N \g_kivi_tab_num_dim
+\dim_gset:Nn \g_kivi_tab_num_dim {5em}
+\dim_new:N \g_kivi_tab_price_dim
+\dim_gset:Nn \g_kivi_tab_price_dim {7em}
+\dim_new:N \g_kivi_tab_desc_dim
+
+\dim_new:N \g_kivi_tabcolsep_dim
+\dim_gset:Nn \g_kivi_tabcolsep_dim {.5\tabcolsep}
+\newcommand*{\CalcTabCols}{
+       \dim_gset:Nn \g_kivi_tab_desc_dim {\textwidth-\g_kivi_tab_pos_dim -\g_kivi_tab_id_dim-\g_kivi_tab_num_dim - 2\g_kivi_tab_price_dim - 10\g_kivi_tabcolsep_dim}
+}
+
+\newcolumntype{P}{>{\raggedleft\arraybackslash}p{\g_kivi_tab_price_dim}<{\,\currency}}
+
+%\if@kivi@faketable
+\RequirePackage{tcolorbox}
+\tcbuselibrary{breakable, skins}
+\seq_new:N \l_kivi_PricingTable_seq
+\seq_new:N \g_kivi_extraDescription_seq
+\newcommand{\FakeTable}[1]{
+       \par
+       \CalcTabCols
+       \seq_set_split:Nnn \l_kivi_PricingTable_seq {\tabularnewline} {#1}
+       \begingroup
+       \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
+       \seq_map_inline:Nn \l_kivi_PricingTable_seq {
+       \seq_gclear:N \g_kivi_extraDescription_seq
+       \exp_args:NnV \use:n {\tabular[t]}\g_kivi_Pricing_colspec_tl
+       ##1
+       \endtabular
+       \seq_if_empty:NTF \g_kivi_extraDescription_seq
+       {\par\nointerlineskip}
+       {\par\nointerlineskip
+       \begin{tcolorbox}[
+               empty,
+               left=\dim_eval:n {\g_kivi_tab_pos_dim+ \g_kivi_tab_id_dim +4\g_kivi_tabcolsep_dim},
+               right=\dim_eval:n {\g_kivi_tab_num_dim+ 2\g_kivi_tab_price_dim +6\g_kivi_tabcolsep_dim},top=0pt,bottom=0pt,
+               boxsep=0pt,
+               breakable,
+               lines~before~break=1,
+       ]
+       \seq_use:Nn \g_kivi_extraDescription_seq {\\}
+       \end{tcolorbox}
+       \nointerlineskip
+       }
+       }
+       \endgroup
+}
+
+
+\tl_new:N \g_kivi_Pricing_colspec_tl
+\tl_gset:Nn \g_kivi_Pricing_colspec_tl {@{}p{\g_kivi_tab_pos_dim}p{\g_kivi_tab_id_dim}p{\g_kivi_tab_desc_dim}>{\raggedleft\arraybackslash}p{\g_kivi_tab_num_dim}*2{P}@{}}
+
+
+\clist_map_inline:nn {head, foot, firsthead, lastfoot} {%TODO reduce
+       \box_new:c {g_kivi_LT@#1_box}
+}
+
+\AtBeginDocument{
+       \csname kivi_setup_LT_boxes:\endcsname
+       \geometry{a4paper,
+               hmargin=\g_kivi_margin_dim,
+               top=\dim_eval:n {\g_kivi_margin_dim + \box_ht:N \g_kivi_LT@head_box},
+               bottom=\dim_eval:n {\g_kivi_margin_dim + \box_ht:N \g_kivi_LT@foot_box},
+               heightrounded}
+       \savegeometry{kivi.letter@table}
+       \loadgeometry{kivi.letter@default}
+}
+
+\cs_new:Nn \kivi_setup_LT_boxes: {
+       \CalcTabCols
+       \hbox_gset:Nn \g_kivi_LT@head_box {
+       \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
+       \exp_args:NnV \use:n {\tabular[b]}\g_kivi_Pricing_colspec_tl
+       \toprule
+       \bfseries\position & \bfseries\artikelnummer & \bfseries\bezeichnung & \bfseries\menge &\multicolumn{1}{P}{\bfseries\einzelpreis}&\multicolumn{1}{P@{}}{\bfseries\gesamtpreis}\\
+       \midrule
+       \endtabular
+       }
+       \hbox_gset:Nn \g_kivi_LT@foot_box {
+       \raisebox{\depth}{
+       \begin{tabular*}{\textwidth}{@{\extracolsep{\fill}}r@{}}
+       \midrule
+       \strut\weiteraufnaechsterseite
+       \end{tabular*}
+       }
+       }
+}
+
+
+%Macht es sinn hier eine Variante zu machen, in der alle Spalten Belegbar sind?
+\newenvironment{PricingTotal}{
+       \tabular[t]{@{}p{\dim_eval:n {\linewidth-\g_kivi_tab_price_dim-2\tabcolsep}}P@{}}
+       \midrule
+}{
+       \bottomrule\endtabular
+}
+
+\newcommand{\ExtraDescription}[1]{\seq_gput_right:Nn \g_kivi_extraDescription_seq {#1}}
+%\else
+\newenvironment{PricingTabular}[1][]{
+       \begingroup
+       \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
+       \CalcTabCols
+       \exp_args:NV \longtable \g_kivi_Pricing_colspec_tl
+       % Tabellenkopf
+       \toprule
+       \bfseries\position & \bfseries\artikelnummer & \bfseries\bezeichnung & \bfseries\menge &\multicolumn{1}{P}{\bfseries\einzelpreis}&\multicolumn{1}{P@{}}{\bfseries\gesamtpreis}\\
+       \midrule
+       \endhead
+       \midrule
+       \multicolumn{6}{@{}r@{}}{\weiteraufnaechsterseite}\\
+       \endfoot
+}{
+       \endlongtable
+       \endgroup
+}
+
+\RequirePackage{xltabular}
+
+
+\newenvironment{SimpleTabular}[1][\bfseries\position & \bfseries\menge & \bfseries\bezeichnung]
+{
+       \setlength{\tabcolsep}{\g_kivi_tabcolsep_dim}
+       \xltabular{\linewidth}{@{}rrX@{}}
+       \toprule
+       #1\\
+       \midrule\\\endhead
+       \midrule
+       \multicolumn{3}{@{}>{\raggedright}p{\linewidth}@{}}{\weiteraufnaechsterseite}\\
+       \endfoot
+       \bottomrule
+       \endlastfoot
+       \ignorespaces
+}{
+       \def\@currenvir{tabularx}
+       \endxltabular
+}
+
+
+\usepackage{afterpage}
+
+\cs_new:cpn {PricingTabular*}{
+       \bool_gset_true:N \g_kivi_inTable_bool
+       \endgroup
+       \@nameuse{Gm@restore@@kivi.letter@table}%
+       \Gm@changelayout
+       \begingroup
+       \def \@currenvir {PricingTabular*}\edef \@currenvline {\on@line }
+       \int_gset:Nn \g_PricingTabular_firstpage_int {\c@page}
+       \addtolength{\vsize}{-\box_ht:N \g_kivi_LT@foot_box}
+       \pagegoal\vsize
+       \widowpenalty0
+       \clubpenalty0
+       \bool_gset_true:N \g_kivi_TableFoot_bool
+       \pagestyle{kivitendo.letter.PricingTable}
+       \leavevmode\box_use:N \g_kivi_LT@head_box
+       \par\nointerlineskip\ignorespaces
+}
+
+\cs_new:cpn {endPricingTabular*} {
+       \int_compare:nNnF \g_PricingTabular_firstpage_int = \c@page {\thispagestyle{kivitendo.letter.PricingTable}}
+       \bool_gset_false:N \g_kivi_TableFoot_bool
+       \@nameuse{Gm@restore@@kivi.letter@default}
+       \Gm@changelayout
+       \bool_gset_true:N \g_kivi_restore_geometry_bool
+       \afterpage{
+               \kivi_conditional_restore_geometry:
+       }
+}
+
+\cs_new:Nn \kivi_conditional_restore_geometry: {
+       \bool_if:NT \g_kivi_restore_geometry_bool
+       {
+       \@nameuse{Gm@restore@@kivi.letter@default}
+       \Gm@changelayout
+       }
+       \bool_gset_false:N \g_kivi_restore_geometry_bool
+}
+
+
+
+\if@kivi@infobox
+\setkomavar{location}{
+       \ifkomavarempty{transaction}{}{
+       \bfseries
+       \usekomavar{transaction}
+       }
+       \par
+       \medskip
+       \begin{tabularx}{\useplength{locwidth}}{@{}l<{:}>{\raggedleft\arraybackslash}X@{}}
+               \usekomavar*{date}&\usekomavar{date}\\
+               \ifkomavarempty{myref}{}{
+                       \usekomavar*{myref}&\usekomavar{myref}\\
+               }
+               \kundennummer&\usekomavar{customer}\\
+               \ifkomavarempty{yourref}{}{
+                       \usekomavar*{yourref}&\usekomavar{yourref}\\
+               }
+               \ifkomavarempty{delivery}{}{
+                       \usekomavar*{delivery}&\usekomavar{delivery}\\
+               }
+               \ifkomavarempty{quote}{}{
+                       \usekomavar*{quote}&\usekomavar{quote}\\
+               }
+               \ifkomavarempty{orderID}{}{\auftragsnummer&\usekomavar{orderID}\\}
+               \ifkomavarempty{projectID}{}{\projektnummer&\usekomavar{projectID}\\}
+               \ansprechpartner&\usekomavar{fromname}
+               \ifkomavarempty{fromphone}{}{\\\textTelefon&\usekomavar{fromphone}}
+               \ifkomavarempty{fromemail}{}{\\\textEmail&\usekomavar{fromemail}}
+       \end{tabularx}
+}
+\removereffields
+\AtBeginLetter{
+       \ifdim\ht\shippingAddressBox>\z@
+       \addtoplength{refvpos}{\ht\shippingAddressBox}
+       \addtoplength{refvpos}{4\baselineskip}
+       \fi
+}
+\ExplSyntaxOff
+\fi
+
+
+
+\renewcommand*{\raggedsignature}{\raggedright}
+
+\endinput
diff --git a/templates/print/marei/kivitendo.sty b/templates/print/marei/kivitendo.sty
new file mode 100644 (file)
index 0000000..32d2e22
--- /dev/null
@@ -0,0 +1,182 @@
+\ProvidesFile{kivitendo.sty}
+\usepackage{colortbl}
+\usepackage{eurosym}
+\usepackage{german}
+\usepackage{graphicx}
+\usepackage{ifthen}
+\usepackage[utf8]{inputenc}
+\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
+\newcommand{\defaultfont}{\fontfamily{cmss}\fontsize{10pt}{12pt}\fontseries{m}\selectfont}
+\renewcommand{\familydefault}{cmss}
+
+%% 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..3b12fb4
--- /dev/null
@@ -0,0 +1,54 @@
+% 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) )$}
+
+
+$( 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..800c63f
--- /dev/null
@@ -0,0 +1,123 @@
+\documentclass[twoside]{scrartcl}
+\usepackage[frame]{xy}
+\usepackage{tabularx}
+\usepackage[utf8]{inputenc}
+\usepackage{graphicx}
+\setlength{\voffset}{0.5cm}
+\setlength{\hoffset}{-2.0cm}
+\setlength{\topmargin}{0cm}
+\setlength{\headheight}{0.5cm}
+\setlength{\headsep}{1cm}
+\setlength{\topskip}{0pt}
+\setlength{\oddsidemargin}{1.0cm}
+\setlength{\evensidemargin}{1.0cm}
+\setlength{\textwidth}{17cm}
+\setlength{\textheight}{24.7cm}
+\setlength{\footskip}{1cm}
+\setlength{\parindent}{0pt}
+\renewcommand{\baselinestretch}{1}
+
+\begin{document}
+
+\newlength{\descrwidth}\setlength{\descrwidth}{9cm}
+\fontfamily{cmss}\fontsize{10pt}{12pt}\selectfont
+
+\pagestyle{myheadings}
+\thispagestyle{empty}
+
+\vspace*{-1.3cm}
+
+\parbox{\textwidth}{
+  \parbox[b]{.42\textwidth}{
+    <%company%>
+
+    <%address%>
+  }\hfill
+  \begin{tabular}[b]{rr@{}}
+  Tel & <%tel%>\\
+  Fax & <%fax%>
+  \end{tabular}
+
+  \rule[1.5ex]{\textwidth}{0.5pt}
+}
+
+\vspace*{0.5cm}
+
+\parbox[t]{1cm}{\hfill}
+\parbox[t]{.5\textwidth}{
+  \textbf{Lieferanschrift}
+} \hfill
+
+\vspace{0.7cm}
+
+\parbox[t]{1cm}{\hfill}
+\parbox[t]{.5\textwidth}{
+
+<%shiptoname%> \\
+<%shiptostreet%> \\
+<%shiptozipcode%> \\
+<%shiptocity%> \\
+<%shiptocountry%>
+}
+\parbox[t]{.4\textwidth}{
+  <%shiptocontact%>
+
+  <%if shiptophone%>
+  Tel: <%shiptophone%>
+  <%end shiptophone%>
+
+  <%if shiptofax%>
+  Fax: <%shiptofax%>
+  <%end shiptofax%>
+
+  <%shiptoemail%>
+}
+\hfill
+
+\vspace{1cm}
+
+\textbf{S A M M E L L I S T E}
+\hfill
+
+\vspace{1cm}
+
+\begin{tabularx}{\textwidth}{*{6}{|X}|} \hline
+  \textbf{BestellNr. \#} & \textbf{Datum} & \textbf{Kontakt}
+  <%if warehouse%>
+  & \textbf{Lager}
+  <%end warehouse%>
+  & \textbf{Lagerplatz} & \textbf{Lieferung mit} \\ [0.5em]
+  \hline
+  <%ordnumber%>
+  <%if shippingdate%>
+  & <%shippingdate%>
+  <%end shippingdate%>
+  <%if not shippingdate%>
+  & <%orddate%>
+  <%end shippingdate%>
+  & <%employee%>
+  <%if warehouse%>
+  & <%warehouse%>
+  <%end warehouse%>
+  & <%shippingpoint%> & <%shipvia%> \\
+  \hline
+\end{tabularx}
+
+\vspace{1cm}
+
+\begin{tabular*}{\textwidth}{@{}rlp{\descrwidth}@{\extracolsep\fill}rcll@{}}
+  \textbf{Pos} & \textbf{Nummer} & \textbf{Beschreibung} &
+  \textbf{Menge} & \textbf{Lagerausgang} & & \textbf{Lagerplatz} \\
+<%foreach number%>
+  <%runningnumber%> & <%number%> & <%description%> &
+  <%qty%> & [\hspace{1cm}] & <%unit%> & <%bin%> \\
+<%end number%>
+\end{tabular*}
+
+
+\parbox{\textwidth}{
+\rule{\textwidth}{2pt}
+}
+
+\end{document}
+
diff --git a/templates/print/marei/proforma.tex b/templates/print/marei/proforma.tex
new file mode 100644 (file)
index 0000000..d591fc4
--- /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 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{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*}
+\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%>
+}
+% Tabellenende letzte Seite
+       \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 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..39e861d
--- /dev/null
@@ -0,0 +1,118 @@
+\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{}{}{\einkaufslieferschein}{<%donumber%>}{<%dodate%>}
+
+
+\begin{document}
+
+\begin{minipage}[t]{8cm}
+  \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%>
+
+  ~
+
+  <%zipcode%> <%city%>
+
+  <%country%>
+\end{minipage}
+\hfill
+\begin{minipage}{6cm}
+  \rightline{\LARGE\textbf{\textit{\einkaufslieferschein}}} \vspace*{0.2cm}
+  \rightline{\large\textbf{\textit{\nr ~<%donumber%>
+  }}} \vspace*{0.2cm}
+
+  \datum:\hfill <%dodate%>
+
+  <%if cusordnumber%>\unsereBestellnummer:\hfill <%cusordnumber%><%end if%>
+
+  <%if ordnumber%>\auftragsnummer:\hfill <%ordnumber%><%end if%>
+
+  \ansprechpartner:\hfill <%employee_name%>
+
+  <%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}
+
+
+%
+% - longtable kann innerhalb der Tabelle umbrechen
+% - da der Umbruch nicht von Lx-Office kontrolliert wird, kann man keinen
+%   Übertrag machen
+% - Innerhalb des Langtextes <%longdescription%> wird nicht umgebrochen.
+%   Falls das gewünscht ist, \\ mit \renewcommand umschreiben (siehe dazu:
+%   http://www.lx-office.org/uploads/media/Lx-Office_Anwendertreffen_LaTeX-Druckvorlagen-31.01.2011_01.pdf)
+%
+\begin{SimpleTabular}
+% eigentliche Tabelle
+<%foreach number%>
+          <%runningnumber%> &
+          <%qty%> <%unit%> &
+          \textbf{<%description%>} \\*  % kein Umbruch nach der ersten Zeile, damit Beschreibung und Langtext nicht getrennt werden
+
+          <%if longdescription%> && \scriptsize <%longdescription%>\\<%end longdescription%>
+          <%if projectnumber%> && \scriptsize \projektnummer: <%projectnumber%>\\<%end projectnumber%>
+          <%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_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{SimpleTabular}
+
+\vspace{0.2cm}
+
+<%if notes%>
+        \vspace{5mm}
+        <%notes%>
+        \vspace{5mm}
+<%end if%>
+
+<%if delivery_term%>
+  \lieferung ~<%delivery_term.description_long%>\\
+<%end delivery_term%>
+
+\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..e4c4b69
--- /dev/null
@@ -0,0 +1,127 @@
+\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%>}
+
+
+\begin{document}
+
+\setkomavar{title}{
+       \bestellung~
+       \nr~<%ordnumber%>
+}
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%orddate%>}
+<%if cusordnumber%>
+\setkomavar*{yourref}{\unsereBestellnummer}
+\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 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{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..6086d45
--- /dev/null
@@ -0,0 +1,71 @@
+\documentclass[twoside]{scrartcl}
+\usepackage[frame]{xy}
+\usepackage{tabularx}
+\usepackage[utf8]{inputenc}
+\setlength{\voffset}{0.4cm}
+\setlength{\hoffset}{-2.0cm}
+\setlength{\topmargin}{0cm}
+\setlength{\headheight}{0.0cm}
+\setlength{\headsep}{1cm}
+\setlength{\topskip}{0pt}
+\setlength{\oddsidemargin}{1.0cm}
+\setlength{\evensidemargin}{1.0cm}
+\setlength{\textwidth}{17cm}
+\setlength{\textheight}{24.5cm}
+\setlength{\footskip}{1cm}
+\setlength{\parindent}{0pt}
+\renewcommand{\baselinestretch}{1}
+\begin{document}
+
+
+\fontfamily{cmss}\fontsize{9pt}{9pt}\selectfont
+
+\parbox[t]{12cm}{
+  <%company%>
+
+  <%address%>}
+\hfill
+\parbox[t]{6cm}{\hfill <%source%>}
+
+\vspace*{0.6cm}
+
+<%text_amount%> \dotfill <%decimal%>/100 \makebox[0.5cm]{\hfill}
+
+\vspace{0.5cm}
+
+\hfill <%datepaid%> \makebox[2cm]{\hfill} <%amount%>
+
+\vspace{0.5cm}
+
+<%name%>
+
+<%street%>
+
+<%zipcode%>
+
+<%city%>
+
+<%country%>
+
+\vspace{2.8cm}
+
+<%company%>
+
+\vspace{0.5cm}
+
+<%name%> \hfill <%datepaid%> \hfill <%source%>
+
+\vspace{0.5cm}
+\begin{tabularx}{\textwidth}{lXrr@{}}
+\textbf{Rechnung} & \textbf{Ausgestellt}
+  & \textbf{Fällig} & \textbf{Verrechnet} \\
+<%foreach invnumber%>
+<%invnumber%> & <%invdate%> \dotfill
+  & <%due%> & <%paid%> \\
+<%end invnumber%>
+\end{tabularx}
+
+\vfill
+
+\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..c7a5140
--- /dev/null
@@ -0,0 +1,134 @@
+\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%>}
+
+
+\begin{document}
+\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{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
+
+
+%
+% - longtable kann innerhalb der Tabelle umbrechen
+% - da der Umbruch nicht von Lx-Office kontrolliert wird, kann man keinen
+%   Übertrag machen
+% - Innerhalb des Langtextes <%longdescription%> wird nicht umgebrochen.
+%   Falls das gewünscht ist, \\ mit \renewcommand umschreiben (siehe dazu:
+%   http://www.lx-office.org/uploads/media/Lx-Office_Anwendertreffen_LaTeX-Druckvorlagen-31.01.2011_01.pdf)
+%
+\setlength\LTleft\parindent     % Tabelle beginnt am linken Textrand
+\setlength\LTright{0pt}         % Tabelle endet am rechten Textrand
+\begin{longtable}{@{}rrp{14cm}@{\extracolsep{\fill}}@{}}
+% Tabellenkopf
+\hline
+\textbf{\position} & \textbf{\menge} & \textbf{\bezeichnung} \\
+\hline\\
+\endhead
+
+% Tabellenkopf erste Seite
+\hline
+\textbf{\position} & \textbf{\menge} & \textbf{\bezeichnung} \\
+\hline\\[-0.5em]
+\endfirsthead
+
+% Tabellenende
+\\
+\multicolumn{3}{@{}r@{}}{\weiteraufnaechsterseite}
+\endfoot
+
+% Tabellenende letzte Seite
+\hline\\
+\endlastfoot
+
+% eigentliche Tabelle
+<%foreach number%>
+          <%runningnumber%> &
+          <%qty%> <%unit%> &
+          \textbf{<%description%>} \\*  % kein Umbruch nach der ersten Zeile, damit Beschreibung und Langtext nicht getrennt werden
+
+          <%if longdescription%> && \scriptsize <%longdescription%>\\<%end longdescription%>
+%          <%if projectnumber%> && \scriptsize \projektnummer: <%projectnumber%>\\<%end projectnumber%>
+          <%if make%>
+            <%foreach make%>
+              \ifstr{<%make%>}{<%name%>}{&& \artikelnummer: <%model%>\\}{}
+            <%end foreach%>
+          <%end if%>
+
+          \\[-0.8em]
+<%end number%>
+
+\end{longtable}
+
+<%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..1d22e9c
--- /dev/null
@@ -0,0 +1,178 @@
+% 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
+
+%% 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..c4879fd
--- /dev/null
@@ -0,0 +1,129 @@
+\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%>}
+
+
+\begin{document}
+
+\setkomavar{title}{
+       \lieferschein~
+       \nr~<%donumber%>
+}
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%dodate%>}
+<%if cusordnumber%>
+       \setkomavar*{yourref}{\unsereBestellnummer}
+       \setkomavar{yourref}{<%cusordnumber%>}
+<%end if%>
+<%if ordnumber%>\setkomavar{orderID}{<%ordnumber%>}<%end if%>
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+<%if globalprojectnumber%>
+  \setkomavar{projectID}{<%globalprojectnumber%>}
+<%end globalprojectnumber%>
+
+\setkomavar{transaction}{<%transaction_description%>}
+
+
+\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{}%muss existieren, damit seitenstil erzeugt wird.
+
+%
+% - longtable kann innerhalb der Tabelle umbrechen
+% - da der Umbruch nicht von Lx-Office kontrolliert wird, kann man keinen
+%   Übertrag machen
+% - Innerhalb des Langtextes <%longdescription%> wird nicht umgebrochen.
+%   Falls das gewünscht ist, \\ mit \renewcommand umschreiben (siehe dazu:
+%   http://www.lx-office.org/uploads/media/Lx-Office_Anwendertreffen_LaTeX-Druckvorlagen-31.01.2011_01.pdf)
+%
+\setlength\LTleft\parindent     % Tabelle beginnt am linken Textrand
+\setlength\LTright{0pt}         % Tabelle endet am rechten Textrand
+\begin{longtable}{@{}rrp{10.7cm}@{\extracolsep{\fill}}r@{}}
+% Tabellenkopf
+\hline
+\textbf{\position} & \textbf{\artikelnummer} & \textbf{\bezeichnung} & \textbf{\menge} \\
+\hline\\
+\endhead
+
+% Tabellenkopf erste Seite
+\hline
+\textbf{\position} & \textbf{\artikelnummer} & \textbf{\bezeichnung} & \textbf{\menge} \\
+\hline\\[-0.5em]
+\endfirsthead
+
+% Tabellenende
+\\
+\multicolumn{4}{@{}r@{}}{\weiteraufnaechsterseite}
+\endfoot
+
+% Tabellenende letzte Seite
+\hline\\
+\endlastfoot
+
+% eigentliche Tabelle
+<%foreach number%>
+          <%runningnumber%> &
+          <%number%> &
+          \textbf{<%description%>}&
+          <%qty%> <%unit%> \\*  % kein Umbruch nach der ersten Zeile, damit Beschreibung und Langtext nicht getrennt werden
+
+          <%if longdescription%> && \scriptsize <%longdescription%>\\<%end longdescription%>
+          <%if reqdate%> && \scriptsize \lieferdatum: <%reqdate%>\\<%end reqdate%>
+          <%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_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{longtable}
+
+
+\vspace{0.2cm}
+
+<%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..2d7ec4e
--- /dev/null
@@ -0,0 +1,136 @@
+\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%>}
+
+
+\begin{document}
+\setkomavar{title}{
+       \auftragsbestaetigung~
+       \nr~<%ordnumber%>
+}
+\setkomavar*{date}{\datum}
+\setkomavar{date}{<%orddate%>}
+<%if cusordnumber%>
+\setkomavar*{yourref}{\ihreBestellnummer}
+\setkomavar{yourref}{<%cusordnumber%>}
+<%end if%>
+<%if quonumber%>\setkomavar{quote}{<%quonumber%>}<%end if%>
+\setkomavar{customer}{<%customernumber%>}
+\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 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{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..bea5afd
--- /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 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{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..16c05eb
--- /dev/null
@@ -0,0 +1,107 @@
+\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}{}{}
+
+
+\begin{document}
+
+\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{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
+
+%
+% - longtable kann innerhalb der Tabelle umbrechen
+% - da der Umbruch nicht von Lx-Office kontrolliert wird, kann man keinen
+%   Übertrag machen
+%
+\setlength\LTleft\parindent     % Tabelle beginnt am linken Textrand
+\setlength\LTright{0pt}         % Tabelle endet am rechten Textrand
+\begin{longtable}{@{\extracolsep{\fill}}rrrrrrr@{}}
+% Tabellenkopf
+\hline
+\textbf{\rechnung~\nr} & \textbf{\datum} & \textbf{\faellig} &
+\textbf{\aktuell} & \textbf{\asDreissig} & \textbf{\asSechzig} & \textbf{\asNeunzig}\\
+\hline\\
+\endhead
+
+% Tabellenkopf erste Seite
+\hline
+\textbf{\rechnung~\nr} & \textbf{\datum} & \textbf{\faellig} &
+\textbf{\aktuell} & \textbf{\asDreissig} & \textbf{\asSechzig} & \textbf{\asNeunzig}\\
+\hline\\[-0.5em]
+\endfirsthead
+
+% Tabellenende
+\\
+\multicolumn{7}{@{}r@{}}{\weiteraufnaechsterseite}
+\endfoot
+
+% Tabellenende letzte Seite
+\hline\\
+\multicolumn{3}{@{}l}{\textbf{\zwischensumme}} & \textbf{<%c0total%>} & \textbf{<%c30total%>} & \textbf{<%c60total%>} & \textbf{<%c90total%>}\\
+\hline\\
+\multicolumn{6}{@{}l}{\textbf{\schlussbetrag}} & \textbf{<%total%>} \\
+\hline\hline\\
+\endlastfoot
+
+% eigentliche Tabelle
+<%foreach invnumber%>
+          <%invnumber%> & <%invdate%> & <%duedate%> &
+          <%c0%> & <%c30%> & <%c60%> & <%c90%> \\
+<%end invnumber%>
+
+\end{longtable}
+
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
+
diff --git a/templates/print/marei/zahlungserinnerung.tex b/templates/print/marei/zahlungserinnerung.tex
new file mode 100644 (file)
index 0000000..429989d
--- /dev/null
@@ -0,0 +1,81 @@
+\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%>}
+
+
+\begin{document}
+
+\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{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}[\bfseries\rechnung~\nr&\bfseries\datum&\bfseries\betrag]
+% eigentliche Tabelle
+<%foreach dn_invnumber%>
+    <%dn_invnumber%> & <%dn_transdate%> & <%dn_amount%> \currency \\[0.1cm]
+<%end dn_invnumber%>
+\end{SimpleTabular}
+
+\vspace{0.2cm}
+
+\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..95468dd
--- /dev/null
@@ -0,0 +1,108 @@
+\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%>}
+
+
+\begin{document}
+
+\setkomavar{title}{
+       \rechnung~
+       \nr ~<%invnumber%>
+}
+\setkomavar*{date}{\rechnungsdatum}
+\setkomavar{date}{<%invdate%>}
+\setkomavar*{myref}{\mahnung~\nr}
+\setkomavar{myref}{<%dunning_id%>}
+\setkomavar{customer}{<%customernumber%>}
+\setkomavar{fromname}{<%employee_name%>}
+\setkomavar{fromphone}{<%employee_tel%>}
+\setkomavar{fromemail}{<%employee_email%>}
+<%if globalprojectnumber%>
+       \setkomavar{projectID}{<%globalprojectnumber%>}
+<%end globalprojectnumber%>
+\setkomavar{transaction}{<%transaction_description%>}
+
+\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
+
+
+
+\setlength\LTleft\parindent     % Tabelle beginnt am linken Textrand
+\setlength\LTright{0pt}         % Tabelle endet am rechten Textrand
+\begin{longtable}{@{}p{7cm}@{\extracolsep{\fill}}r@{}}
+% Tabellenkopf
+\hline
+\textbf{\posten} & \textbf{\betrag} \\
+\hline\\
+\endhead
+
+% Tabellenkopf erste Seite
+\hline
+\textbf{\posten} & \textbf{\betrag} \\
+\hline\\[-0.5em]
+\endfirsthead
+
+% Tabellenende
+\\
+\multicolumn{2}{@{}r@{}}{\weiteraufnaechsterseite}
+\endfoot
+
+% Tabellenende letzte Seite
+\hline\\
+\multicolumn{1}{@{}l}{\schlussbetrag} & <%invamount%> \currency\\
+\hline\hline\\
+\endlastfoot
+
+% eigentliche Tabelle
+Mahngebühren & <%fee%> \currency \\
+Zinsen & <%interest%> \currency \\
+\\[-0.8em]
+
+\end{longtable}
+
+
+\vspace{0.2cm}
+
+\bitteZahlenBis~<%duedate%>.
+
+\closing{\gruesse}
+
+\end{letter}
+
+\end{document}
index 6f5dea7..6e65c4d 100644 (file)
         [% 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>
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 %]">
index 4b8ed2d..980d229 100644 (file)
@@ -1,5 +1,6 @@
 [%- USE T8 %]
 [%- USE HTML %]
+[% INCLUDE "common/flash.html" %]
  <h1>[% title %]</h1>
 
  <table>
index fd8c91e..e569004 100644 (file)
@@ -42,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 %]">
                 <th align="right" nowrap>[% 'Due Date' | $T8 %]</th>
                 <td>[% L.date_tag('duedate', duedate) %]</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>
index 3ee796f..601cbb2 100644 (file)
        [% 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>
            <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>
index bb939a5..e24771b 100644 (file)
@@ -2,6 +2,7 @@
 
   [% 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">
 
 [% 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") } ] ;
index 75bc22e..c1908a4 100644 (file)
                 [% 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, 5)) %][% LxERP.format_amount(exchangerate, 5) %][%- ELSE %][% L.input_tag('exchangerate', LxERP.format_amount(exchangerate, 5), 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>
               [% IF ALL_DEPARTMENTS %]
                 <th align=right nowrap>[% 'Due Date' | $T8 %]</th>
                 <td>[% L.date_tag('duedate', duedate) %]</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, onChange = "document.getElementById('update_button').click();") %]</td>
 [%- 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>
index ee9669c..35a8a3a 100644 (file)
    <td>[% P.chart.picker('defaults.ar_chart_id', SELF.defaults.ar_chart_id, type='AR', choose=1, style=style) %]<td>
   </tr>
 
- </table>
+  <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>
 </div>
index 09c8075..ec622e5 100644 (file)
    <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>[% 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("Create sales invoices with ZUGFeRD data") %]</td>
+   <td>[% L.select_tag("defaults.create_zugferd_invoices", [ [ 0, LxERP.t8('Do not create ZUGFeRD invoices') ], [ 1, LxERP.t8('Create ZUGFeRD invoices') ], [ 2, LxERP.t8('Create ZUGFeRD invoices in test mode') ] ],
+                       default=SELF.defaults.create_zugferd_invoices) %]</td>
+   <td>
+     [% LxERP.t8("If enabled ZUGFeRD-conformant sales invoice PDFs will be created.") %]
+     [% LxERP.t8("If the test mode is enabled, the ZUGFeRD invoices will be flagged so that they're only fit to be used for testing purposes.") %]
+   </td>
+  </tr>
 
   <tr><td class="listheading" colspan="4">[% LxERP.t8("E-mail") %]</td></tr>
 
index 35a9728..f938f72 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>
index 5758acc..3cc28ef 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 align="right">[% LxERP.t8('Use default warehouse for assembly transfer') %]</td>
    <td>
index c49aef7..cb21c4f 100644 (file)
  [% 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>
 </table>
 
 <p>
- [% L.button_tag("kivi.SalesPurchase.submit_custom_shipto()", LxERP.t8("Apply")) %]
+ [% 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")) %]
index 96b69c6..d400371 100644 (file)
@@ -13,7 +13,7 @@
     [% '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) %]
 [%- ELSIF ( var.config.type == 'date' ) %]
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 75fae98..aecbf1d 100644 (file)
     [%- 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' OR SELF.type == 'ar_transactions' %]
+[%- 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>
+   [%- 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 == … %]
 
    <p>
  [%- 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' %]
    <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>
 
index 77e6960..8bb3930 100644 (file)
@@ -7,6 +7,9 @@
  [%- 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 %]
index a5c43fb..4fdc8a3 100644 (file)
 
   [%- 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>
       <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>
-[%- 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 %]
       [% 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 ) %]
         <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') %]
     [% 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" %]
@@ -66,6 +75,9 @@
     [% 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>
 </form>
 
index b06b517..2169771 100644 (file)
       <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>
         [% L.checkbox_tag('cv.order_lock', checked = SELF.cv.order_lock, for_submit=1) %]
       </td>
+      <th align="right">[% LxERP.t8("Create sales invoices with ZUGFeRD data") %]</th>
+      <td>[% L.select_tag("cv.create_zugferd_invoices",
+                          [ [ -1, LxERP.t8('Use settings from client configuration') ],
+                            [ 0, LxERP.t8('Do not create ZUGFeRD invoices') ],
+                            [ 1, LxERP.t8('Create ZUGFeRD invoices') ],
+                            [ 2, LxERP.t8('Create ZUGFeRD invoices in test mode') ] ],
+                          default=SELF.cv.create_zugferd_invoices) %]</td>
      </tr>
     [% END %]
   </table>
index edf5fa2..90793f4 100644 (file)
       <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>
 
       <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>
 
 
   </table>
 
-  [% L.button_tag('submitInputButton("delete_contact");', LxERP.t8('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();
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 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 55dc7df..f7e8ef1 100644 (file)
           <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>[%- LxERP.format_amount(row.amount, 2) %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
           <td>[% row.duedate.to_kivitendo | html %]</td>
-          <td>[%- LxERP.format_amount(row.paid, 2) %]</td>
-          <td>[%- LxERP.format_amount(row.amount - row.paid,2) %]
+          <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 != '' %]
index 0ebf8a1..d15933a 100644 (file)
@@ -38,7 +38,7 @@
               [%- END -%]
             [% END %]
         <td>[% row.transdate.to_kivitendo | html %]</td>
-        <td>[%- LxERP.format_amount(row.amount, 2) %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
         <td>[% row.employee.name | html %]</td>
         <td>[% row.transaction_description | html %]</td>
       </tr>
index cf793e2..6df5e45 100644 (file)
@@ -16,9 +16,9 @@
       <tr class="listrow[% loop.count % 2 %]">
         <td>[% row.date_part | html %]</td>
         <td>[% row.count | html %]</td>
-        <td>[%- LxERP.format_amount(row.amount,2) %]</td>
-        <td>[%- LxERP.format_amount(row.netamount,2) %]</td>
-        <td>[%- LxERP.format_amount(row.paid,2) %]</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 %]
 
index c6dc91a..8d6e61b 100644 (file)
           <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>[%- LxERP.format_amount(row.amount, 2) %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
           <td>[% row.duedate.to_kivitendo | html %]</td>
-          <td>[%- LxERP.format_amount(row.paid, 2) %]</td>
-          <td>[%- LxERP.format_amount(row.amount - row.paid, 2) %]
+          <td class="numeric">[%- LxERP.format_amount(row.paid, 2) %]</td>
+          <td class="numeric">[%- LxERP.format_amount(row.amount - row.paid, 2) %]
         </tr>
       [% END %]
     </tbody>
index 77aefc0..c295699 100644 (file)
         [%- 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>
+          <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>[%- LxERP.format_amount(row.amount, 2) %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
         <td>[% row.reqdate.to_kivitendo | html %]</td>
         <td>[% row.transaction_description %]</td>
       </tr>
index 0c37bd5..02b808d 100644 (file)
           <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) %]'>
+          <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>[%- LxERP.format_amount(row.amount, 2) %]</td>
+        <td class="numeric">[%- LxERP.format_amount(row.amount, 2) %]</td>
         <td>[% row.reqdate.to_kivitendo | html %]</td>
         <td>[% row.transaction_description %]</td>
       </tr>
index 68d8f07..1ad546c 100644 (file)
@@ -67,7 +67,7 @@
 
  [%- IF !delivered %]
   <div id="shipto_inputs" class="hidden">
-   [%- PROCESS 'common/_ship_to_dialog.html' vc_obj=VC_OBJ %]
+   [%- PROCESS 'common/_ship_to_dialog.html' vc_obj=VC_OBJ cvars=shipto_cvars %]
   </div>
  [%- END %]
 
index 6e7c499..59fa98e 100644 (file)
   <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) %]">
-[%- FOREACH row = HIDDENS %]
-   [% L.hidden_tag(row.name, row.value) %]
-[%- END %]
 
   <p>
    <table width="100%">
index ab1c44a..d061d2c 100644 (file)
@@ -1,4 +1,4 @@
-[%- USE T8 %]
+[%- USE T8 %][%- USE L %]
 [% USE HTML %]<script type="text/javascript" src="js/common.js"></script>
 <h1>[% title %]</h1>
 
     </td>
    </tr>
 
+   [% IF SHOW_DEPARTMENT_SELECTION %]
+    <tr>
+     <th align="right">[% 'Department' | $T8 %]</th>
+     <td colspan="3">
+     [% L.select_tag('department_id', ALL_DEPARTMENTS, title_key = 'description', with_empty = 1, style=style) %]
+     </td>
+    </tr>
+   [% END %]
+
    [% IF SHOW_DUNNING_LEVEL_SELECTION %]
     <tr>
      <th align="right">[% 'Next Dunning Level' | $T8 %]</th>
@@ -59,5 +68,9 @@
     <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>
  </form>
index 1cef95f..19686f0 100644 (file)
@@ -16,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>
@@ -45,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>
@@ -76,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>
index 51fbb32..bfa979d 100644 (file)
@@ -5,6 +5,7 @@
 
 [% 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 %]
+[% 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">
 
   <h2>[% LxERP.t8("Print options") %]</h2>
     [% 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>
@@ -62,7 +72,9 @@
 
      <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="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) %]">
@@ -86,6 +98,6 @@
 
   <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) %]">
  </form>
index bd347aa..6b37127 100644 (file)
  <script type="text/javascript">
    function calculate_qty() {
 [%- FOREACH row = VARIABLES %]
-     var [% row.name %] = parse_amount('[% MYCONFIG.numberformat %]', $('#calc_qty_form_id #[% row.name %]').val());
+     var [% row.name %] = kivi.parse_amount($('#calc_qty_form_id #[% row.name %]').val());
 [%- END %]
      var result = [% formel %];
-     result = number_format(result, 2, '[% MYCONFIG.numberformat %]');
+     result = kivi.format_amount(result, 2);
      if (document.CalcQtyForm.input_id.value) {
        document.getElementById(document.CalcQtyForm.input_id.value).value = result;
      } else {
      $('#calc_qty_dialog').dialog('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];
-     }
-     return arr_int[0] + sep + arr_int[1];
-   }
  </script>
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..04f1051
--- /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">[% '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 e8b7b5a..644ff96 100644 (file)
@@ -67,7 +67,7 @@
             <table>
               <tr>
                 <th align=right width=50% nowrap>[% 'Transdate' | $T8 %]</th>
-                <td>[% L.date_tag('transdate', transdate, readonly=readonly) %]</td>
+                <td align=left>[% L.date_tag('transdate', transdate, readonly=readonly) %]</td>
               </tr>
             </table>
           </td>
           <table>
               <tr>
                 <th align=right width=50%>[% 'Gldate' | $T8 %]</th>
-                <td align=left>[% L.date_tag('gldate', gldate, readonly=1) %]</td>
+                [%-# hidden img to keep alignment -%]
+                <td align=left>[% L.date_tag('gldate', gldate, readonly=1) %]<img class="ui-datepicker-trigger" src="image/calendar.png" alt="..." title="..." style='visibility:hidden'></td>
               </tr>
             </table>
           </td>
         </tr>
 [%- END %]
 
-[%- IF ALL_DEPARTMENTS %]
+        [% SET departments_style = "";
+           SET departments_style = "style='visibility:hidden'" IF ALL_DEPARTMENTS.size == 0 %]
         <tr>
-          <th align=right nowrap>[% 'Department' | $T8 %]</th>
-          <td>[% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1) %]</td>
+          <th [%- departments_style -%]align=right nowrap>[% 'Department' | $T8 %]</th>
+          <td [%- departments_style -%]>[% L.select_tag('department_id', ALL_DEPARTMENTS, default = department_id, title_key = 'description', with_empty = 1) %]</td>
+          <td align=left>
+            <table>
+              <tr>
+                <th align=right width=50% nowrap>[% 'Delivery Date' | $T8 %]</th>
+                <td align=left>[% L.date_tag('deliverydate', deliverydate) %]</td>
+              </tr>
+            </table>
         </tr>
-[%- END %]
 
         <tr>
           <th align=right width=1%>[% 'Description' | $T8 %]</th>
index 88d8668..72ce6ef 100644 (file)
@@ -1,4 +1,4 @@
 [% USE L %][%- USE LxERP -%]
 [% FOR row = TAX_ACCOUNTS %]
-<option value='[% row.id %]--[% row.rate %]'[% IF row.is_default %] selected[% END %]>[% row.taxdescription %] [% LxERP.round_amount(row.rate * 100) %] %</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/gl/yearend_bottom.html b/templates/webpages/gl/yearend_bottom.html
deleted file mode 100644 (file)
index 1a8fb82..0000000
+++ /dev/null
@@ -1,12 +0,0 @@
-[%- USE L %]
-[%- USE LxERP %]
-    [%- L.hidden_tag("cb_date",SELF.cb_date.to_kivitendo) %]
-    [%- L.hidden_tag("cb_startdate",SELF.cb_startdate.to_kivitendo) %]
-    [%- L.hidden_tag("cb_reference",SELF.cb_reference) %]
-    [%- L.hidden_tag("cb_description",SELF.cb_description) %]
-    [%- L.hidden_tag("ob_date",SELF.ob_date.to_kivitendo) %]
-    [%- L.hidden_tag("ob_reference",SELF.ob_reference) %]
-    [%- L.hidden_tag("ob_description",SELF.ob_description) %]
-    [%- L.hidden_tag("cbob_chart",SELF.cbob_chart) %]
-    [%- L.hidden_tag("rowcount",SELF.row_count) %]
-</form>
diff --git a/templates/webpages/gl/yearend_filter.html b/templates/webpages/gl/yearend_filter.html
deleted file mode 100644 (file)
index 566454d..0000000
+++ /dev/null
@@ -1,59 +0,0 @@
-[%- USE HTML %]
-[%- USE T8 %]
-[%- USE L %]
-[%- USE LxERP %]
-
-<h1>[% title | html %]</h1>
-
-[%- INCLUDE 'common/flash.html' %]
-
-<form id='filter_form'>
-
-<table>
-  <tr>
-    <td width="20px"></td>
-    <td align="left" colspan="5">[% 'Attention: Here will be generated a lot of CB/OB transactions.' | $T8 %]</td>
-  </tr>
-  <tr>
-    <td><p></p></td>
-  </tr>
-  <tr>
-    <th></th>
-    <th width="400px" colspan="2" align="center">[% 'CB Transactions' | $T8 %]</th>
-    <th width="400px"colspan="2" align="center">[% 'OB Transactions' | $T8 %]</th>
-    <th>&nbsp;</th>
-  </tr>
-  <tr>
-    <td></td>
-    <td align="right">[% 'Date' | $T8 %]</td>
-    <td>[% L.date_tag('cb_date', SELF.cb_date) %]</td>
-    <td align="right">[% 'Date' | $T8 %]</td>
-    <td>[% L.date_tag('ob_date', SELF.ob_date) %]</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td></td>
-    <td align="right">[% 'Reference' | $T8 %]</td>
-    <td>[% L.input_tag('cb_reference', SELF.cb_reference) %]</td>
-    <td align="right">[% 'Reference' | $T8 %]</td>
-    <td>[% L.input_tag('ob_reference', SELF.ob_reference) %]</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td></td>
-    <td align="right">[% 'Description' | $T8 %]</td>
-    <td>[% L.input_tag('cb_description', SELF.cb_description) %]</td>
-    <td align="right">[% 'Description' | $T8 %]</td>
-    <td>[% L.input_tag('ob_description', SELF.ob_description) %]</td>
-    <td></td>
-  </tr>
-  <tr>
-    <td><p></p></td>
-  </tr>
-  <tr>
-    <th colspan="2"></th>
-    <th align=right>[% 'close chart' | $T8 %]</th>
-    <td colspan="3">[% L.select_tag('cbob_chart', SELF.charts9000, title_sub=\make_title_of_chart, default=SELF.cbob_chart, style="width: 400px") %]</td>
-  </tr>
-</table>
-</form>
diff --git a/templates/webpages/gl/yearend_top.html b/templates/webpages/gl/yearend_top.html
deleted file mode 100644 (file)
index 59cca0e..0000000
+++ /dev/null
@@ -1,22 +0,0 @@
-[%- USE LxERP %]
-<form method="post" action="controller.pl" id="form">
-<table>
-  <tr>
-    <td width="20px"></td>
-    <td colspan="6" align="left">
-      [%- LxERP.t8('Select charts for which the CB/OB transactions want to be posted.') %]<br>
-      [%- LxERP.t8('There will be two transactions done:') %]<br>
-     - [%- LxERP.t8('One SB-transaction') %] ( [% SELF.cb_date.to_kivitendo %], [% SELF.cb_reference %], [% SELF.cb_description %], [% SELF.cbob_chartaccno %] )<br>
-     - [%- LxERP.t8('One OB-transaction') %] ( [% SELF.ob_date.to_kivitendo %], [% SELF.ob_reference %], [% SELF.ob_description %], [% SELF.cbob_chartaccno %] )<br>
-      [%- LxERP.t8('No revert available.') %]
-    </td>
-  </tr>
-  <tr>
-    <td><p></p></td>
-  </tr>
-  <tr>
-    <th></th>
-    <th align="right">[% LxERP.t8('close chart') %]</th>
-    <td>[% SELF.cbob_chartaccno %]</td>
-  </tr>
-</table>
index 3e68dc0..42ec5fb 100644 (file)
        <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>
 
index d47ecd4..6726c8b 100644 (file)
   [% 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 %]">
index f9880ad..30f77c5 100644 (file)
          </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>
index 4389cd1..d146c7b 100644 (file)
           <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 %]
            <span id="duedate_fixed"[% IF !payment_terms_obj.auto_calculation %] style="display:none"[% END %]>[% HTML.escape(duedate) %]</span>
           </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>
index 23400b5..93a0fb8 100644 (file)
   [% 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 %]">
index 5990f49..c0f7363 100644 (file)
           <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 %]
index 7ae4c2b..20c63f1 100644 (file)
@@ -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>
index 481ce17..08caf09 100644 (file)
      <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('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>
index bc88963..c382a5c 100644 (file)
 
   [% L.hidden_tag("action", "MassInvoiceCreatePrint/dispatch") %]
   [% L.hidden_tag("printer_id") %]
+  [% L.hidden_tag("bothsided") %]
  </form>
 
  [% 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") } ] ;
index 70d2e16..9f1cc1b 100644 (file)
                     <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 %]
index b8d013b..45a6f0a 100644 (file)
     <tr>
      <th align="right">[% 'Transaction description' | $T8 %]</th>
      <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' | $T8 %]</th>
      <td>[% P.project.picker("project_id", '', style=style) %]</td>
-
      <th align="right">[% 'Part Number' | $T8 %]</th>
      <td>[% L.input_tag("parts_partnumber", "", style=style) %]</td>
     </tr>
          <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>
+        </td>
+
        </tr>
        <tr>
         <td>
index 6020fd7..c3ff986 100644 (file)
@@ -13,6 +13,8 @@
   </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) %]
     <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>
index 9c6b572..d5c0ad0 100644 (file)
@@ -4,6 +4,7 @@
   <table id="input_row_table_id">
     <thead>
       <tr class="listheading">
+        <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>
     </thead>
     <tbody>
       <tr valign="top" class="listrow">
-        <td>[% P.part.picker('add_item.parts_id', '', fat_set_item=1, style='width: 300px', class="add_item_input") %]</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', '', fat_set_item=1, style='width: 300px', class="add_item_input", $PARAM_KEY=PARAM_VAL) %]</td>
         <td>[% L.input_tag('add_item.description', '', class="add_item_input") %]</td>
         <td>
           [% L.input_tag('add_item.qty_as_number', '', size = 5, class="add_item_input numeric") %]
index 3719a93..77cc1e5 100644 (file)
@@ -18,6 +18,8 @@
 <div id='multi_items_result'></div>
 <hr>
 
+[% 'At position' | $T8 %]
+[% L.input_tag('multi_items.position', '', size = 5, class="numeric") %]</td>
 [% L.button_tag('kivi.Order.add_multi_items()', LxERP.t8('Continue'), id='multi_items_dialog_continue_button') %]
 <a href="#" onclick="kivi.Order.close_multi_items_dialog();">[%- LxERP.t8("Cancel") %]</a>
 
index be05c62..02f1fd3 100644 (file)
@@ -4,7 +4,16 @@
 [%- USE LxERP %]
 [% SET best_price = price_source.best_price %]
 [% SET best_discount = price_source.best_discount %]
-[% SET price_editable = AUTH.assert('edit_prices', 1) %]
+[% 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>
@@ -12,6 +21,9 @@
     <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>
 [%- 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, -2) _ '\', ' _ price_editable _ ')', LxERP.t8('Select')) %]</td>
+     <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, -2) _ '\', ' _ price_editable _ ')', LxERP.t8('Update Price')) %]</td>
+     <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 %]
index effb32d..b93b728 100644 (file)
@@ -4,7 +4,7 @@
 [%- USE L %]
 [%- USE P %]
 
-<tbody class="row_entry listrow"[%- IF MYCONFIG.show_form_details -%] data-expanded="1"[%- END -%]>
+<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 %]
                        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">[% HTML.escape(ITEM.part.partnumber) %]</div>
+      <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>
@@ -48,7 +64,7 @@
       [%- END -%]
       [%- L.button_tag("kivi.Order.show_longdescription_dialog(this)", LxERP.t8("L")) %]
     </td>
-    [%- IF (TYPE == "sales_order" || TYPE == "purchase_order") -%]
+    [%- 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>
@@ -65,7 +81,7 @@
     </td>
     <td>
       [%- L.select_tag("order.orderitems[].price_factor_id",
-                       ALL_PRICE_FACTORS,
+                       SELF.all_price_factors,
                        default = ITEM.price_factor_id,
                        title_key = 'description',
                        with_empty = 1,
                        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 = (AUTH.assert('edit_prices', 1) && 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,
     </td>
     <td>
       [%- L.hidden_tag("order.orderitems[].active_discount_source", ITEM.active_discount_source.source) %]
-      [%- SET EDIT_DISCOUNT = (AUTH.assert('edit_prices', 1) && 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,
     <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=TYPE %]
+          [%- PROCESS order/tabs/_second_row.html ITEM=ITEM TYPE=SELF.type %]
         </div>
       [%- ELSE %]
         <div name="second_row" id="second_row_[% ID %]">
index 7966271..b7086a8 100644 (file)
                                 style='width: 300px') %]</td>
           </tr>
 
-          <tr id='shipto_row' [%- IF !SELF.order.${SELF.cv}.shipto.size %]style='display:none'[%- END %]>
+          <tr>
             <th align="right">[% 'Shipping Address' | $T8 %]</th>
-            <td>[% L.select_tag('order.shipto_id',
-                                SELF.order.${SELF.cv}.shipto,
-                                default=SELF.order.shipto_id,
-                                title_key='displayable_id',
-                                value_key='shipto_id',
-                                with_empty=1,
-                                style='width: 300px') %]</td>
+            <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>
 
           [%- PROCESS order/tabs/_business_info_row.html SELF=SELF %]
             <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_departments.size %]
           <tr>
             <th align="right">[% 'Department' | $T8 %]</th>
@@ -70,7 +94,7 @@
 
           <tr>
             <th align="right">[% 'Transaction description' | $T8 %]</th>
-            <td>[% L.input_tag('order.transaction_description', SELF.order.transaction_description, style='width: 300px') %]</td>
+            <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>
     </tr>
   </table>
 
-  [%- PROCESS order/tabs/_item_input.html %]
+  [%- PROCESS order/tabs/_item_input.html SELF=SELF %]
 
   [% L.button_tag('kivi.Order.show_multi_items_dialog()', LxERP.t8('Add multiple items')) %]
 
                 <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>
-                <th id="partnumber_header_id"  class="listheading" nowrap width="15"><a href='#' onClick='javascript:kivi.Order.reorder_items("partnumber")'> [%- 'Partnumber'  | $T8 %]</a></th>
-                <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.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>
+                <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 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>
             </thead>
 
             [%- FOREACH item = SELF.order.items_sorted %]
-              [%- PROCESS order/tabs/_row.html ITEM=item ID=(item.id||item.new_fake_id) TYPE=SELF.type ALL_PRICE_FACTORS=SELF.all_price_factors %]
+              [%- PROCESS order/tabs/_row.html ITEM=item ID=(item.id||item.new_fake_id)  -%]
             [%- END %]
 
           </table>
index fca9bb7..9e7fcee 100644 (file)
@@ -28,7 +28,7 @@
              <tr>
               <th align="right">[% 'Part Description' | $T8 %]</th>
               <td>
-               [% L.areainput_tag("part.description", SELF.part.description, size=40) %]</td>
+               [% L.areainput_tag("part.description", SELF.part.description, size=40) %]
               </td>
              </tr>
              <tr>
           <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>
+            [%- 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 %]
            <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') %]</td>
+            [%- 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>
 
-<div id="pricegroups">
  [% PROCESS 'part/_pricegroup_prices.html' %]
-</div>
-
-<div id="customerprices">
  [% PROCESS 'part/_customerprices.html' %]
-</div>
 [%- UNLESS SELF.part.is_assembly %]
-<div id="makemodel">
  [% PROCESS 'part/_makemodel.html' %]
-</div>
 [% END %]
 
   <tr>
index 49f32c3..3f7eeb6 100644 (file)
@@ -8,6 +8,7 @@
     <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">[% '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 %]
+        [%- FOREACH customerprice = SELF.part.customerprices_sorted %]
         [% listrow = listrow + 1 %]
         [% PROCESS 'part/_customerprice_row.html' customerprice=customerprice listrow=listrow %]
         [%- END %]
index 601bb10..bdc68ee 100644 (file)
   [%- FOREACH language = SELF.all_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.hidden_tag('translations[+].language_id', language.id) %]
+      [% 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>
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 %]
index e56a886..e157977 100644 (file)
@@ -9,6 +9,7 @@
     <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>
@@ -17,6 +18,7 @@
           <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 %]
index e2a90df..49916e0 100644 (file)
@@ -5,7 +5,7 @@
 <table id='multi_items_filter_table'>
   <tr>
     <th>[%- LxERP.t8("Description") %]/[%- LxERP.t8("Partnumber") %]:</th>
-    <td>[%- L.input_tag('multi_items.filter.all:substr:multi::ilike', SELF.multi_items_models.filtered.laundered.all_substr_multi__ilike) %]</td>
+    <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>
index 733fb13..05e73f3 100644 (file)
@@ -38,6 +38,9 @@
     [%- 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 %]
      [% 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 %]
index aa50c7d..49b08c4 100644 (file)
@@ -5,7 +5,7 @@
 
 <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') %]
+[% 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')) %]
index 02d54d9..3918ee6 100644 (file)
@@ -32,14 +32,23 @@ 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_id8', undef, multiple=1, status='obsolete') %]<br>
+<br>
+All parts including make models of all vendors: <br>
+[% P.part.picker('part_id', undef, with_makemodel=1) %]<br>
+All parts including make models of all vendors with multi select popup: <br>
+[% P.part.picker('part_id', undef, with_makemodel=1, multiple=1) %]<br>
+All parts including customer partnumbers of all customers: <br>
+[% P.part.picker('part_id', undef, with_customer_partnumber=1) %]<br>
 
 <h2>Styling</h2>
 
index f3da6cc..21e58fc 100644 (file)
   <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 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('Obsolete') %]</th>
   <td>[% L.checkbox_tag('object.obsolete', checked = SELF.object.obsolete, for_submit=1) %]</td>
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 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 6092496..9d39515 100644 (file)
@@ -19,6 +19,7 @@
           <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>
             <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 b9d125e..8400452 100644 (file)
@@ -93,7 +93,7 @@
        </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>
index ada16af..9d79535 100644 (file)
@@ -1,6 +1,7 @@
 [%- USE T8 %]
 [%- USE L %]
 [%- USE P %]
+[%- USE LxERP %]
 [%- USE HTML %][%- USE JavaScript %]
 <h1>[% 'Report about warehouse contents' | $T8 %]</h1>
 
@@ -95,7 +96,7 @@
        </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>
         <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>
+        <th align="right">
+          [% "List all rows" | $T8 %]:
+        </th>
+        <td align="left">
+         [% L.yes_no_tag("allrows", 1) %]
+        </td>
+       </tr>
+
       </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>
         <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>
      </td>
     </tr>
index 5bfdad9..efab904 100644 (file)
@@ -55,7 +55,7 @@
 
       $(function() {
         warehouse_selected(0, 0);
-        document.Form.partnumber.focus();
+        document.Form.part_id_name.focus();
       });
      -->
  </script>
@@ -96,7 +96,7 @@
     <tr>
      <th align="right" nowrap>[% 'Part' | $T8 %]</th>
      <td>
-      [% P.part.picker("part_id", '', size="30", part_type="part,assembly") %]
+      [% P.part.picker("part_id", parts_id, size="30", part_type="part,assembly") %]
      </td>
     </tr>
 
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..dd8662f
--- /dev/null
@@ -0,0 +1,15 @@
+[%- USE HTML %]
+[%- USE LxERP %]
+[%- USE L %]
+[%- USE T8 %]
+[%- INCLUDE 'common/flash.html' %]
+ <div class="listtop">[% FORM.title %]</div>
+
+ <p>
+ [% "Import a 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'.