Merge branch 'develop' into sales-analytics
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 9979377..72149a6 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2016-06-01 14:38:51.012597",
@@ -63,6 +64,7 @@
"cost_center",
"section_break_12",
"status",
+ "custom_remarks",
"remarks",
"column_break_16",
"letter_head",
@@ -462,7 +464,8 @@
"fieldname": "remarks",
"fieldtype": "Small Text",
"label": "Remarks",
- "no_copy": 1
+ "no_copy": 1,
+ "read_only_depends_on": "eval:doc.custom_remarks == 0"
},
{
"fieldname": "column_break_16",
@@ -573,10 +576,18 @@
"label": "Status",
"options": "\nDraft\nSubmitted\nCancelled",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "custom_remarks",
+ "fieldtype": "Check",
+ "label": "Custom Remarks"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-12-08 13:02:30.016610",
+ "links": [],
+ "modified": "2020-09-02 13:39:43.383705",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index bb312bf..11ab020 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -453,7 +453,7 @@
frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction"))
def set_remarks(self):
- if self.remarks: return
+ if self.custom_remarks: return
if self.payment_type=="Internal Transfer":
remarks = [_("Amount {0} {1} transferred from {2} to {3}")
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 2bfa4a5..fe5301d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -28,7 +28,7 @@
// Trigger supplier event on load if supplier is available
// The reason for this is PI can be created from PR or PO and supplier is pre populated
- if (this.frm.doc.supplier) {
+ if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
},
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 69817a4..158799c 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -202,6 +202,53 @@
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name)
frappe.set_user("Administrator")
+ def test_update_child_with_tax_template(self):
+ tax_template = "_Test Account Excise Duty @ 10"
+ item = "_Test Item Home Desktop 100"
+
+ if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
+ item_doc = frappe.get_doc("Item", item)
+ item_doc.append("taxes", {
+ "item_tax_template": tax_template,
+ "valid_from": nowdate()
+ })
+ item_doc.save()
+ else:
+ # update valid from
+ frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
+ where parent = %(item)s and item_tax_template = %(tax)s""",
+ {"item": item, "tax": tax_template})
+
+ po = create_purchase_order(item_code=item, qty=1, do_not_save=1)
+
+ po.append("taxes", {
+ "account_head": "_Test Account Excise Duty - _TC",
+ "charge_type": "On Net Total",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Excise Duty",
+ "doctype": "Purchase Taxes and Charges",
+ "rate": 10
+ })
+ po.insert()
+ po.submit()
+
+ self.assertEqual(po.taxes[0].tax_amount, 50)
+ self.assertEqual(po.taxes[0].total, 550)
+
+ items = json.dumps([
+ {'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name},
+ {'item_code' : item, 'rate' : 100, 'qty' : 1} # added item
+ ])
+ update_child_qty_rate('Purchase Order', items, po.name)
+
+ po.reload()
+ self.assertEqual(po.taxes[0].tax_amount, 60)
+ self.assertEqual(po.taxes[0].total, 660)
+
+ frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
+ where parent = %(item)s and item_tax_template = %(tax)s""",
+ {"item": item, "tax": tax_template})
+
def test_update_child_uom_conv_factor_change(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 9093cd5..046fb2c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -20,7 +20,7 @@
from erpnext.exceptions import InvalidCurrency
from six import text_type
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
-from erpnext.stock.get_item_details import get_item_warehouse
+from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
@@ -1158,6 +1158,18 @@
}
return info
+def set_child_tax_template_and_map(item, child_item, parent_doc):
+ args = {
+ 'item_code': item.item_code,
+ 'posting_date': parent_doc.transaction_date,
+ 'tax_category': parent_doc.get('tax_category'),
+ 'company': parent_doc.get('company')
+ }
+
+ child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
+ if child_item.get("item_tax_template"):
+ child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
+
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
"""
Returns a Sales Order Item child item containing the default values
@@ -1172,6 +1184,7 @@
child_item.uom = trans_item.get("uom") or item.stock_uom
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
+ set_child_tax_template_and_map(item, child_item, p_doc)
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
@@ -1195,6 +1208,7 @@
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
child_item.base_rate = 1 # Initiallize value will update in parent validation
child_item.base_amount = 1 # Initiallize value will update in parent validation
+ set_child_tax_template_and_map(item, child_item, p_doc)
return child_item
def validate_and_delete_children(parent, data):
@@ -1232,7 +1246,7 @@
frappe.throw(_("You do not have permissions to {} items in a {}.")
.format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions"))
-
+
def validate_workflow_conditions(doc):
workflow = get_workflow_name(doc.doctype)
if not workflow:
@@ -1267,7 +1281,7 @@
sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation']
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
-
+
check_doc_permissions(parent, 'cancel')
validate_and_delete_children(parent, data)
@@ -1315,7 +1329,7 @@
child_item.conversion_factor = 1
else:
child_item.conversion_factor = flt(d.get('conversion_factor'))
-
+
if d.get("uom"):
child_item.uom = d.get("uom")
conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor"))
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 6d58fd2..be30086 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -595,7 +595,7 @@
$.each(actual_taxes_dict, function(key, value) {
if (value) total_actual_tax += value;
});
-
+
return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total"));
}
},
diff --git a/erpnext/regional/germany/utils/__init__.py b/erpnext/regional/germany/utils/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/regional/germany/utils/__init__.py
diff --git a/erpnext/regional/germany/utils/datev/__init__.py b/erpnext/regional/germany/utils/datev/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/regional/germany/utils/datev/__init__.py
diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py
similarity index 89%
rename from erpnext/regional/report/datev/datev_constants.py
rename to erpnext/regional/germany/utils/datev/datev_constants.py
index e063703..63f9a77 100644
--- a/erpnext/regional/report/datev/datev_constants.py
+++ b/erpnext/regional/germany/utils/datev/datev_constants.py
@@ -460,80 +460,8 @@
"Sprach-ID"
]
-QUERY_REPORT_COLUMNS = [
- {
- "label": "Umsatz (ohne Soll/Haben-Kz)",
- "fieldname": "Umsatz (ohne Soll/Haben-Kz)",
- "fieldtype": "Currency",
- "width": 100
- },
- {
- "label": "Soll/Haben-Kennzeichen",
- "fieldname": "Soll/Haben-Kennzeichen",
- "fieldtype": "Data",
- "width": 100
- },
- {
- "label": "Konto",
- "fieldname": "Konto",
- "fieldtype": "Data",
- "width": 100
- },
- {
- "label": "Gegenkonto (ohne BU-Schlüssel)",
- "fieldname": "Gegenkonto (ohne BU-Schlüssel)",
- "fieldtype": "Data",
- "width": 100
- },
- {
- "label": "Belegdatum",
- "fieldname": "Belegdatum",
- "fieldtype": "Date",
- "width": 100
- },
- {
- "label": "Belegfeld 1",
- "fieldname": "Belegfeld 1",
- "fieldtype": "Data",
- "width": 150
- },
- {
- "label": "Buchungstext",
- "fieldname": "Buchungstext",
- "fieldtype": "Text",
- "width": 300
- },
- {
- "label": "Beleginfo - Art 1",
- "fieldname": "Beleginfo - Art 1",
- "fieldtype": "Link",
- "options": "DocType",
- "width": 100
- },
- {
- "label": "Beleginfo - Inhalt 1",
- "fieldname": "Beleginfo - Inhalt 1",
- "fieldtype": "Dynamic Link",
- "options": "Beleginfo - Art 1",
- "width": 150
- },
- {
- "label": "Beleginfo - Art 2",
- "fieldname": "Beleginfo - Art 2",
- "fieldtype": "Link",
- "options": "DocType",
- "width": 100
- },
- {
- "label": "Beleginfo - Inhalt 2",
- "fieldname": "Beleginfo - Inhalt 2",
- "fieldtype": "Dynamic Link",
- "options": "Beleginfo - Art 2",
- "width": 150
- }
-]
-
class DataCategory():
+
"""Field of the CSV Header."""
DEBTORS_CREDITORS = "16"
@@ -542,6 +470,7 @@
POSTING_TEXT_CONSTANTS = "67"
class FormatName():
+
"""Field of the CSV Header, corresponds to DataCategory."""
DEBTORS_CREDITORS = "Debitoren/Kreditoren"
diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py
new file mode 100644
index 0000000..aae734f
--- /dev/null
+++ b/erpnext/regional/germany/utils/datev/datev_csv.py
@@ -0,0 +1,174 @@
+# coding: utf-8
+from __future__ import unicode_literals
+
+import datetime
+import zipfile
+from csv import QUOTE_NONNUMERIC
+from six import BytesIO
+
+import six
+import frappe
+import pandas as pd
+from frappe import _
+from .datev_constants import DataCategory
+
+
+def get_datev_csv(data, filters, csv_class):
+ """
+ Fill in missing columns and return a CSV in DATEV Format.
+
+ For automatic processing, DATEV requires the first line of the CSV file to
+ hold meta data such as the length of account numbers oder the category of
+ the data.
+
+ Arguments:
+ data -- array of dictionaries
+ filters -- dict
+ csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS
+ """
+ empty_df = pd.DataFrame(columns=csv_class.COLUMNS)
+ data_df = pd.DataFrame.from_records(data)
+ result = empty_df.append(data_df, sort=True)
+
+ if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS:
+ result['Belegdatum'] = pd.to_datetime(result['Belegdatum'])
+
+ if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES:
+ result['Sprach-ID'] = 'de-DE'
+
+ data = result.to_csv(
+ # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035
+ sep=str(';'),
+ # European decimal seperator
+ decimal=',',
+ # Windows "ANSI" encoding
+ encoding='latin_1',
+ # format date as DDMM
+ date_format='%d%m',
+ # Windows line terminator
+ line_terminator='\r\n',
+ # Do not number rows
+ index=False,
+ # Use all columns defined above
+ columns=csv_class.COLUMNS,
+ # Quote most fields, even currency values with "," separator
+ quoting=QUOTE_NONNUMERIC
+ )
+
+ if not six.PY2:
+ data = data.encode('latin_1')
+
+ header = get_header(filters, csv_class)
+ header = ';'.join(header).encode('latin_1')
+
+ # 1st Row: Header with meta data
+ # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here.
+ # 3rd - nth Row: Data (Nutzdaten)
+ return header + b'\r\n' + data
+
+
+def get_header(filters, csv_class):
+ description = filters.get('voucher_type', csv_class.FORMAT_NAME)
+ company = filters.get('company')
+ datev_settings = frappe.get_doc('DATEV Settings', {'client': company})
+ default_currency = frappe.get_value('Company', company, 'default_currency')
+ coa = frappe.get_value('Company', company, 'chart_of_accounts')
+ coa_short_code = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '')
+
+ header = [
+ # DATEV format
+ # "DTVF" = created by DATEV software,
+ # "EXTF" = created by other software
+ '"EXTF"',
+ # version of the DATEV format
+ # 141 = 1.41,
+ # 510 = 5.10,
+ # 720 = 7.20
+ '700',
+ csv_class.DATA_CATEGORY,
+ '"%s"' % csv_class.FORMAT_NAME,
+ # Format version (regarding format name)
+ csv_class.FORMAT_VERSION,
+ # Generated on
+ datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '000',
+ # Imported on -- stays empty
+ '',
+ # Origin. Any two symbols, will be replaced by "SV" on import.
+ '"EN"',
+ # I = Exported by
+ '"%s"' % frappe.session.user,
+ # J = Imported by -- stays empty
+ '',
+ # K = Tax consultant number (Beraternummer)
+ datev_settings.get('consultant_number', '0000000'),
+ # L = Tax client number (Mandantennummer)
+ datev_settings.get('client_number', '00000'),
+ # M = Start of the fiscal year (Wirtschaftsjahresbeginn)
+ frappe.utils.formatdate(frappe.defaults.get_user_default('year_start_date'), 'yyyyMMdd'),
+ # N = Length of account numbers (Sachkontenlänge)
+ datev_settings.get('account_number_length', '4'),
+ # O = Transaction batch start date (YYYYMMDD)
+ frappe.utils.formatdate(filters.get('from_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # P = Transaction batch end date (YYYYMMDD)
+ frappe.utils.formatdate(filters.get('to_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # Q = Description (for example, "Sales Invoice") Max. 30 chars
+ '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # R = Diktatkürzel
+ '',
+ # S = Buchungstyp
+ # 1 = Transaction batch (Finanzbuchführung),
+ # 2 = Annual financial statement (Jahresabschluss)
+ '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # T = Rechnungslegungszweck
+ # 0 oder leer = vom Rechnungslegungszweck unabhängig
+ # 50 = Handelsrecht
+ # 30 = Steuerrecht
+ # 64 = IFRS
+ # 40 = Kalkulatorik
+ # 11 = Reserviert
+ # 12 = Reserviert
+ '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # U = Festschreibung
+ # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1"
+ '0',
+ # V = Default currency, for example, "EUR"
+ '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
+ # reserviert
+ '',
+ # Derivatskennzeichen
+ '',
+ # reserviert
+ '',
+ # reserviert
+ '',
+ # SKR
+ '"%s"' % coa_short_code,
+ # Branchen-Lösungs-ID
+ '',
+ # reserviert
+ '',
+ # reserviert
+ '',
+ # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung)
+ ''
+ ]
+ return header
+
+
+def download_csv_files_as_zip(csv_data_list):
+ """
+ Put CSV files in a zip archive and send that to the client.
+
+ Params:
+ csv_data_list -- list of dicts [{'file_name': 'EXTF_Buchunsstapel.zip', 'csv_data': get_datev_csv()}]
+ """
+ zip_buffer = BytesIO()
+
+ datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
+ for csv_file in csv_data_list:
+ datev_zip.writestr(csv_file.get('file_name'), csv_file.get('csv_data'))
+ datev_zip.close()
+
+ frappe.response['filecontent'] = zip_buffer.getvalue()
+ frappe.response['filename'] = 'DATEV.zip'
+ frappe.response['type'] = 'binary'
diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
index 7fec94e..dd818e6 100644
--- a/erpnext/regional/report/datev/datev.py
+++ b/erpnext/regional/report/datev/datev.py
@@ -9,31 +9,91 @@
"""
from __future__ import unicode_literals
-import datetime
import json
-import zipfile
-import six
import frappe
-import pandas as pd
-
from frappe import _
-from csv import QUOTE_NONNUMERIC
-from six import BytesIO
from six import string_types
-from .datev_constants import DataCategory
-from .datev_constants import Transactions
-from .datev_constants import DebtorsCreditors
-from .datev_constants import AccountNames
-from .datev_constants import QUERY_REPORT_COLUMNS
+from erpnext.regional.germany.utils.datev.datev_csv import download_csv_files_as_zip, get_datev_csv
+from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames
+
+COLUMNS = [
+ {
+ "label": "Umsatz (ohne Soll/Haben-Kz)",
+ "fieldname": "Umsatz (ohne Soll/Haben-Kz)",
+ "fieldtype": "Currency",
+ "width": 100
+ },
+ {
+ "label": "Soll/Haben-Kennzeichen",
+ "fieldname": "Soll/Haben-Kennzeichen",
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "label": "Konto",
+ "fieldname": "Konto",
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "label": "Gegenkonto (ohne BU-Schlüssel)",
+ "fieldname": "Gegenkonto (ohne BU-Schlüssel)",
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "label": "Belegdatum",
+ "fieldname": "Belegdatum",
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "label": "Belegfeld 1",
+ "fieldname": "Belegfeld 1",
+ "fieldtype": "Data",
+ "width": 150
+ },
+ {
+ "label": "Buchungstext",
+ "fieldname": "Buchungstext",
+ "fieldtype": "Text",
+ "width": 300
+ },
+ {
+ "label": "Beleginfo - Art 1",
+ "fieldname": "Beleginfo - Art 1",
+ "fieldtype": "Link",
+ "options": "DocType",
+ "width": 100
+ },
+ {
+ "label": "Beleginfo - Inhalt 1",
+ "fieldname": "Beleginfo - Inhalt 1",
+ "fieldtype": "Dynamic Link",
+ "options": "Beleginfo - Art 1",
+ "width": 150
+ },
+ {
+ "label": "Beleginfo - Art 2",
+ "fieldname": "Beleginfo - Art 2",
+ "fieldtype": "Link",
+ "options": "DocType",
+ "width": 100
+ },
+ {
+ "label": "Beleginfo - Inhalt 2",
+ "fieldname": "Beleginfo - Inhalt 2",
+ "fieldtype": "Dynamic Link",
+ "options": "Beleginfo - Art 2",
+ "width": 150
+ }
+]
def execute(filters=None):
"""Entry point for frappe."""
validate(filters)
- result = get_transactions(filters, as_dict=0)
- columns = QUERY_REPORT_COLUMNS
-
- return columns, result
+ return COLUMNS, get_transactions(filters, as_dict=0)
def validate(filters):
@@ -240,146 +300,8 @@
""", filters, as_dict=1)
-def get_datev_csv(data, filters, csv_class):
- """
- Fill in missing columns and return a CSV in DATEV Format.
-
- For automatic processing, DATEV requires the first line of the CSV file to
- hold meta data such as the length of account numbers oder the category of
- the data.
-
- Arguments:
- data -- array of dictionaries
- filters -- dict
- csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS
- """
- empty_df = pd.DataFrame(columns=csv_class.COLUMNS)
- data_df = pd.DataFrame.from_records(data)
-
- result = empty_df.append(data_df, sort=True)
-
- if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS:
- result['Belegdatum'] = pd.to_datetime(result['Belegdatum'])
-
- if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES:
- result['Sprach-ID'] = 'de-DE'
-
- data = result.to_csv(
- # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035
- sep=str(';'),
- # European decimal seperator
- decimal=',',
- # Windows "ANSI" encoding
- encoding='latin_1',
- # format date as DDMM
- date_format='%d%m',
- # Windows line terminator
- line_terminator='\r\n',
- # Do not number rows
- index=False,
- # Use all columns defined above
- columns=csv_class.COLUMNS,
- # Quote most fields, even currency values with "," separator
- quoting=QUOTE_NONNUMERIC
- )
-
- if not six.PY2:
- data = data.encode('latin_1')
-
- header = get_header(filters, csv_class)
- header = ';'.join(header).encode('latin_1')
-
- # 1st Row: Header with meta data
- # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here.
- # 3rd - nth Row: Data (Nutzdaten)
- return header + b'\r\n' + data
-
-
-def get_header(filters, csv_class):
- description = filters.get('voucher_type', csv_class.FORMAT_NAME)
-
- header = [
- # DATEV format
- # "DTVF" = created by DATEV software,
- # "EXTF" = created by other software
- '"EXTF"',
- # version of the DATEV format
- # 141 = 1.41,
- # 510 = 5.10,
- # 720 = 7.20
- '700',
- csv_class.DATA_CATEGORY,
- '"%s"' % csv_class.FORMAT_NAME,
- # Format version (regarding format name)
- csv_class.FORMAT_VERSION,
- # Generated on
- datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000',
- # Imported on -- stays empty
- '',
- # Origin. Any two symbols, will be replaced by "SV" on import.
- '"EN"',
- # I = Exported by
- '"%s"' % frappe.session.user,
- # J = Imported by -- stays empty
- '',
- # K = Tax consultant number (Beraternummer)
- filters.get('consultant_number', '0000000'),
- # L = Tax client number (Mandantennummer)
- filters.get('client_number', '00000'),
- # M = Start of the fiscal year (Wirtschaftsjahresbeginn)
- frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"),
- # N = Length of account numbers (Sachkontenlänge)
- '%d' % filters.get('acc_len', 4),
- # O = Transaction batch start date (YYYYMMDD)
- frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd") if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # P = Transaction batch end date (YYYYMMDD)
- frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd") if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # Q = Description (for example, "Sales Invoice") Max. 30 chars
- '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # R = Diktatkürzel
- '',
- # S = Buchungstyp
- # 1 = Transaction batch (Finanzbuchführung),
- # 2 = Annual financial statement (Jahresabschluss)
- '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # T = Rechnungslegungszweck
- # 0 oder leer = vom Rechnungslegungszweck unabhängig
- # 50 = Handelsrecht
- # 30 = Steuerrecht
- # 64 = IFRS
- # 40 = Kalkulatorik
- # 11 = Reserviert
- # 12 = Reserviert
- '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # U = Festschreibung
- # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1"
- '0',
- # V = Default currency, for example, "EUR"
- '"%s"' % filters.get('default_currency', 'EUR') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '',
- # reserviert
- '',
- # Derivatskennzeichen
- '',
- # reserviert
- '',
- # reserviert
- '',
- # SKR
- '"%s"' % filters.get('skr', '04'),
- # Branchen-Lösungs-ID
- '',
- # reserviert
- '',
- # reserviert
- '',
- # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung)
- ''
- ]
- return header
-
-
@frappe.whitelist()
-def download_datev_csv(filters=None):
+def download_datev_csv(filters):
"""
Provide accounting entries for download in DATEV format.
@@ -400,38 +322,26 @@
coa = frappe.get_value('Company', filters.get('company'), 'chart_of_accounts')
filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '')
- # set account number length
- account_numbers = frappe.get_list('Account', fields=['account_number'], filters={'is_group': 0, 'account_number': ('!=', '')})
- filters['acc_len'] = max([len(a.account_number) for a in account_numbers])
-
- filters['consultant_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'consultant_number')
- filters['client_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'client_number')
- filters['default_currency'] = frappe.get_value('Company', filters.get('company'), 'default_currency')
-
- # This is where my zip will be written
- zip_buffer = BytesIO()
- # This is my zip file
- datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED)
-
transactions = get_transactions(filters)
- transactions_csv = get_datev_csv(transactions, filters, csv_class=Transactions)
- datev_zip.writestr('EXTF_Buchungsstapel.csv', transactions_csv)
-
account_names = get_account_names(filters)
- account_names_csv = get_datev_csv(account_names, filters, csv_class=AccountNames)
- datev_zip.writestr('EXTF_Kontenbeschriftungen.csv', account_names_csv)
-
customers = get_customers(filters)
- customers_csv = get_datev_csv(customers, filters, csv_class=DebtorsCreditors)
- datev_zip.writestr('EXTF_Kunden.csv', customers_csv)
-
suppliers = get_suppliers(filters)
- suppliers_csv = get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors)
- datev_zip.writestr('EXTF_Lieferanten.csv', suppliers_csv)
-
- # You must call close() before exiting your program or essential records will not be written.
- datev_zip.close()
- frappe.response['filecontent'] = zip_buffer.getvalue()
- frappe.response['filename'] = 'DATEV.zip'
- frappe.response['type'] = 'binary'
+ download_csv_files_as_zip([
+ {
+ 'file_name': 'EXTF_Buchungsstapel.csv',
+ 'csv_data': get_datev_csv(transactions, filters, csv_class=Transactions)
+ },
+ {
+ 'file_name': 'EXTF_Kontenbeschriftungen.csv',
+ 'csv_data': get_datev_csv(account_names, filters, csv_class=AccountNames)
+ },
+ {
+ 'file_name': 'EXTF_Kunden.csv',
+ 'csv_data': get_datev_csv(customers, filters, csv_class=DebtorsCreditors)
+ },
+ {
+ 'file_name': 'EXTF_Lieferanten.csv',
+ 'csv_data': get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors)
+ },
+ ])
diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py
index eed62a8..9529923 100644
--- a/erpnext/regional/report/datev/test_datev.py
+++ b/erpnext/regional/report/datev/test_datev.py
@@ -1,32 +1,22 @@
# coding=utf-8
from __future__ import unicode_literals
-import os
-import json
import zipfile
+import frappe
from six import BytesIO
from unittest import TestCase
-
-import frappe
-from frappe.utils import getdate, today, now_datetime, cstr
-from frappe.test_runner import make_test_objects
+from frappe.utils import today, now_datetime, cstr
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts
from erpnext.regional.report.datev.datev import validate
from erpnext.regional.report.datev.datev import get_transactions
from erpnext.regional.report.datev.datev import get_customers
from erpnext.regional.report.datev.datev import get_suppliers
from erpnext.regional.report.datev.datev import get_account_names
-from erpnext.regional.report.datev.datev import get_datev_csv
-from erpnext.regional.report.datev.datev import get_header
from erpnext.regional.report.datev.datev import download_datev_csv
-from erpnext.regional.report.datev.datev_constants import DataCategory
-from erpnext.regional.report.datev.datev_constants import Transactions
-from erpnext.regional.report.datev.datev_constants import DebtorsCreditors
-from erpnext.regional.report.datev.datev_constants import AccountNames
-from erpnext.regional.report.datev.datev_constants import QUERY_REPORT_COLUMNS
+from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, get_header
+from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames
def make_company(company_name, abbr):
if not frappe.db.exists("Company", company_name):