2019-06-14 06:19:20 +00:00
|
|
|
# coding: utf-8
|
|
|
|
"""
|
|
|
|
Provide a report and downloadable CSV according to the German DATEV format.
|
|
|
|
|
|
|
|
- Query report showing only the columns that contain data, formatted nicely for
|
|
|
|
dispay to the user.
|
|
|
|
- CSV download functionality `download_datev_csv` that provides a CSV file with
|
|
|
|
all required columns. Used to import the data into the DATEV Software.
|
|
|
|
"""
|
|
|
|
from __future__ import unicode_literals
|
2020-05-11 16:50:02 +00:00
|
|
|
|
2019-06-14 06:19:20 +00:00
|
|
|
import json
|
2020-05-11 16:50:02 +00:00
|
|
|
import frappe
|
2019-06-14 06:19:20 +00:00
|
|
|
from six import string_types
|
2020-11-09 14:47:56 +00:00
|
|
|
|
|
|
|
from frappe import _
|
2020-11-05 15:57:23 +00:00
|
|
|
from erpnext.accounts.utils import get_fiscal_year
|
2020-11-09 14:47:56 +00:00
|
|
|
from erpnext.regional.germany.utils.datev.datev_csv import zip_and_download, get_datev_csv
|
2020-07-29 15:14:23 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
]
|
2019-06-14 06:19:20 +00:00
|
|
|
|
|
|
|
|
|
|
|
def execute(filters=None):
|
|
|
|
"""Entry point for frappe."""
|
2019-08-13 22:39:59 +00:00
|
|
|
validate(filters)
|
2020-07-29 15:14:23 +00:00
|
|
|
return COLUMNS, get_transactions(filters, as_dict=0)
|
2019-06-14 06:19:20 +00:00
|
|
|
|
|
|
|
|
2019-08-13 22:39:59 +00:00
|
|
|
def validate(filters):
|
|
|
|
"""Make sure all mandatory filters and settings are present."""
|
2020-11-05 15:57:23 +00:00
|
|
|
company = filters.get('company')
|
|
|
|
if not company:
|
2019-08-13 23:15:23 +00:00
|
|
|
frappe.throw(_('<b>Company</b> is a mandatory filter.'))
|
2019-06-14 06:19:20 +00:00
|
|
|
|
2020-11-05 15:57:23 +00:00
|
|
|
from_date = filters.get('from_date')
|
|
|
|
if not from_date:
|
2019-08-13 23:15:23 +00:00
|
|
|
frappe.throw(_('<b>From Date</b> is a mandatory filter.'))
|
2019-06-14 06:19:20 +00:00
|
|
|
|
2020-11-05 15:57:23 +00:00
|
|
|
to_date = filters.get('to_date')
|
|
|
|
if not to_date:
|
2019-08-13 23:15:23 +00:00
|
|
|
frappe.throw(_('<b>To Date</b> is a mandatory filter.'))
|
2019-06-14 06:19:20 +00:00
|
|
|
|
2020-11-05 15:57:23 +00:00
|
|
|
validate_fiscal_year(from_date, to_date, company)
|
|
|
|
|
2019-08-13 22:39:59 +00:00
|
|
|
try:
|
2019-08-13 23:22:29 +00:00
|
|
|
frappe.get_doc('DATEV Settings', filters.get('company'))
|
2019-08-13 22:39:59 +00:00
|
|
|
except frappe.DoesNotExistError:
|
2019-08-13 23:15:23 +00:00
|
|
|
frappe.throw(_('Please create <b>DATEV Settings</b> for Company <b>{}</b>.').format(filters.get('company')))
|
2019-06-14 06:19:20 +00:00
|
|
|
|
|
|
|
|
2020-11-05 15:57:23 +00:00
|
|
|
def validate_fiscal_year(from_date, to_date, company):
|
|
|
|
from_fiscal_year = get_fiscal_year(date=from_date, company=company)
|
|
|
|
to_fiscal_year = get_fiscal_year(date=to_date, company=company)
|
|
|
|
if from_fiscal_year != to_fiscal_year:
|
|
|
|
frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date))
|
|
|
|
|
|
|
|
|
2019-11-29 12:02:17 +00:00
|
|
|
def get_transactions(filters, as_dict=1):
|
2019-06-14 06:19:20 +00:00
|
|
|
"""
|
|
|
|
Get a list of accounting entries.
|
|
|
|
|
|
|
|
Select GL Entries joined with Account and Party Account in order to get the
|
|
|
|
account numbers. Returns a list of accounting entries.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
filters -- dict of filters to be passed to the sql query
|
|
|
|
as_dict -- return as list of dicts [0,1]
|
|
|
|
"""
|
2020-03-31 05:10:03 +00:00
|
|
|
filter_by_voucher = 'AND gl.voucher_type = %(voucher_type)s' if filters.get('voucher_type') else ''
|
2019-06-14 06:19:20 +00:00
|
|
|
gl_entries = frappe.db.sql("""
|
2019-11-29 12:02:17 +00:00
|
|
|
SELECT
|
2019-06-14 06:19:20 +00:00
|
|
|
|
|
|
|
/* either debit or credit amount; always positive */
|
|
|
|
case gl.debit when 0 then gl.credit else gl.debit end as 'Umsatz (ohne Soll/Haben-Kz)',
|
|
|
|
|
|
|
|
/* 'H' when credit, 'S' when debit */
|
|
|
|
case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen',
|
|
|
|
|
|
|
|
/* account number or, if empty, party account number */
|
2020-02-13 19:58:44 +00:00
|
|
|
coalesce(acc.account_number, acc_pa.account_number) as 'Konto',
|
2019-06-14 06:19:20 +00:00
|
|
|
|
|
|
|
/* against number or, if empty, party against number */
|
|
|
|
coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)',
|
|
|
|
|
|
|
|
gl.posting_date as 'Belegdatum',
|
2020-02-13 19:58:44 +00:00
|
|
|
gl.voucher_no as 'Belegfeld 1',
|
2020-04-08 03:56:18 +00:00
|
|
|
LEFT(gl.remarks, 60) as 'Buchungstext',
|
2020-03-31 05:10:03 +00:00
|
|
|
gl.voucher_type as 'Beleginfo - Art 1',
|
|
|
|
gl.voucher_no as 'Beleginfo - Inhalt 1',
|
|
|
|
gl.against_voucher_type as 'Beleginfo - Art 2',
|
|
|
|
gl.against_voucher as 'Beleginfo - Inhalt 2'
|
2019-06-14 06:19:20 +00:00
|
|
|
|
2019-11-29 12:02:17 +00:00
|
|
|
FROM `tabGL Entry` gl
|
2019-06-14 06:19:20 +00:00
|
|
|
|
|
|
|
/* Statistisches Konto (Debitoren/Kreditoren) */
|
|
|
|
left join `tabParty Account` pa
|
|
|
|
on gl.against = pa.parent
|
|
|
|
and gl.company = pa.company
|
|
|
|
|
|
|
|
/* Kontonummer */
|
|
|
|
left join `tabAccount` acc
|
|
|
|
on gl.account = acc.name
|
|
|
|
|
|
|
|
/* Gegenkonto-Nummer */
|
|
|
|
left join `tabAccount` acc_against
|
|
|
|
on gl.against = acc_against.name
|
|
|
|
|
|
|
|
/* Statistische Kontonummer */
|
|
|
|
left join `tabAccount` acc_pa
|
|
|
|
on pa.account = acc_pa.name
|
|
|
|
|
|
|
|
/* Statistische Gegenkonto-Nummer */
|
|
|
|
left join `tabAccount` acc_against_pa
|
|
|
|
on pa.account = acc_against_pa.name
|
|
|
|
|
2019-11-29 12:02:17 +00:00
|
|
|
WHERE gl.company = %(company)s
|
|
|
|
AND DATE(gl.posting_date) >= %(from_date)s
|
|
|
|
AND DATE(gl.posting_date) <= %(to_date)s
|
2020-03-31 05:10:03 +00:00
|
|
|
{}
|
|
|
|
ORDER BY 'Belegdatum', gl.voucher_no""".format(filter_by_voucher), filters, as_dict=as_dict)
|
2019-06-14 06:19:20 +00:00
|
|
|
|
|
|
|
return gl_entries
|
|
|
|
|
|
|
|
|
2019-11-29 12:02:17 +00:00
|
|
|
def get_customers(filters):
|
|
|
|
"""
|
|
|
|
Get a list of Customers.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
filters -- dict of filters to be passed to the sql query
|
|
|
|
"""
|
|
|
|
return frappe.db.sql("""
|
|
|
|
SELECT
|
|
|
|
|
|
|
|
acc.account_number as 'Konto',
|
2020-05-11 17:15:49 +00:00
|
|
|
CASE cus.customer_type WHEN 'Company' THEN cus.customer_name ELSE null END as 'Name (Adressatentyp Unternehmen)',
|
|
|
|
CASE cus.customer_type WHEN 'Individual' THEN con.last_name ELSE null END as 'Name (Adressatentyp natürl. Person)',
|
|
|
|
CASE cus.customer_type WHEN 'Individual' THEN con.first_name ELSE null END as 'Vorname (Adressatentyp natürl. Person)',
|
|
|
|
CASE cus.customer_type WHEN 'Individual' THEN '1' WHEN 'Company' THEN '2' ELSE '0' end as 'Adressatentyp',
|
2019-11-29 12:02:17 +00:00
|
|
|
adr.address_line1 as 'Straße',
|
|
|
|
adr.pincode as 'Postleitzahl',
|
|
|
|
adr.city as 'Ort',
|
|
|
|
UPPER(country.code) as 'Land',
|
|
|
|
adr.address_line2 as 'Adresszusatz',
|
|
|
|
con.email_id as 'E-Mail',
|
|
|
|
coalesce(con.mobile_no, con.phone) as 'Telefon',
|
|
|
|
cus.website as 'Internet',
|
2020-05-11 17:15:49 +00:00
|
|
|
cus.tax_id as 'Steuernummer'
|
2019-11-29 12:02:17 +00:00
|
|
|
|
|
|
|
FROM `tabParty Account` par
|
|
|
|
|
|
|
|
left join `tabAccount` acc
|
|
|
|
on acc.name = par.account
|
|
|
|
|
|
|
|
left join `tabCustomer` cus
|
|
|
|
on cus.name = par.parent
|
|
|
|
|
|
|
|
left join `tabAddress` adr
|
|
|
|
on adr.name = cus.customer_primary_address
|
|
|
|
|
|
|
|
left join `tabCountry` country
|
|
|
|
on country.name = adr.country
|
|
|
|
|
|
|
|
left join `tabContact` con
|
|
|
|
on con.name = cus.customer_primary_contact
|
|
|
|
|
|
|
|
WHERE par.company = %(company)s
|
2020-03-26 07:55:58 +00:00
|
|
|
AND par.parenttype = 'Customer'""", filters, as_dict=1)
|
2019-11-29 12:02:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_suppliers(filters):
|
|
|
|
"""
|
|
|
|
Get a list of Suppliers.
|
|
|
|
|
|
|
|
Arguments:
|
|
|
|
filters -- dict of filters to be passed to the sql query
|
|
|
|
"""
|
|
|
|
return frappe.db.sql("""
|
|
|
|
SELECT
|
|
|
|
|
|
|
|
acc.account_number as 'Konto',
|
2020-05-11 17:15:49 +00:00
|
|
|
CASE sup.supplier_type WHEN 'Company' THEN sup.supplier_name ELSE null END as 'Name (Adressatentyp Unternehmen)',
|
|
|
|
CASE sup.supplier_type WHEN 'Individual' THEN con.last_name ELSE null END as 'Name (Adressatentyp natürl. Person)',
|
|
|
|
CASE sup.supplier_type WHEN 'Individual' THEN con.first_name ELSE null END as 'Vorname (Adressatentyp natürl. Person)',
|
|
|
|
CASE sup.supplier_type WHEN 'Individual' THEN '1' WHEN 'Company' THEN '2' ELSE '0' end as 'Adressatentyp',
|
2019-11-29 12:02:17 +00:00
|
|
|
adr.address_line1 as 'Straße',
|
|
|
|
adr.pincode as 'Postleitzahl',
|
|
|
|
adr.city as 'Ort',
|
|
|
|
UPPER(country.code) as 'Land',
|
|
|
|
adr.address_line2 as 'Adresszusatz',
|
|
|
|
con.email_id as 'E-Mail',
|
|
|
|
coalesce(con.mobile_no, con.phone) as 'Telefon',
|
|
|
|
sup.website as 'Internet',
|
|
|
|
sup.tax_id as 'Steuernummer',
|
|
|
|
case sup.on_hold when 1 then sup.release_date else null end as 'Zahlungssperre bis'
|
|
|
|
|
|
|
|
FROM `tabParty Account` par
|
|
|
|
|
|
|
|
left join `tabAccount` acc
|
|
|
|
on acc.name = par.account
|
|
|
|
|
|
|
|
left join `tabSupplier` sup
|
|
|
|
on sup.name = par.parent
|
|
|
|
|
|
|
|
left join `tabDynamic Link` dyn_adr
|
|
|
|
on dyn_adr.link_name = sup.name
|
|
|
|
and dyn_adr.link_doctype = 'Supplier'
|
|
|
|
and dyn_adr.parenttype = 'Address'
|
|
|
|
|
|
|
|
left join `tabAddress` adr
|
|
|
|
on adr.name = dyn_adr.parent
|
|
|
|
and adr.is_primary_address = '1'
|
|
|
|
|
|
|
|
left join `tabCountry` country
|
|
|
|
on country.name = adr.country
|
|
|
|
|
|
|
|
left join `tabDynamic Link` dyn_con
|
|
|
|
on dyn_con.link_name = sup.name
|
|
|
|
and dyn_con.link_doctype = 'Supplier'
|
|
|
|
and dyn_con.parenttype = 'Contact'
|
|
|
|
|
|
|
|
left join `tabContact` con
|
|
|
|
on con.name = dyn_con.parent
|
|
|
|
and con.is_primary_contact = '1'
|
|
|
|
|
|
|
|
WHERE par.company = %(company)s
|
2020-03-26 07:55:58 +00:00
|
|
|
AND par.parenttype = 'Supplier'""", filters, as_dict=1)
|
2019-11-29 12:02:17 +00:00
|
|
|
|
|
|
|
|
|
|
|
def get_account_names(filters):
|
2020-05-11 17:15:03 +00:00
|
|
|
return frappe.db.sql("""
|
|
|
|
SELECT
|
|
|
|
|
|
|
|
account_number as 'Konto',
|
|
|
|
LEFT(account_name, 40) as 'Kontenbeschriftung',
|
|
|
|
'de-DE' as 'Sprach-ID'
|
|
|
|
|
|
|
|
FROM `tabAccount`
|
|
|
|
WHERE company = %(company)s
|
|
|
|
AND is_group = 0
|
|
|
|
AND account_number != ''
|
|
|
|
""", filters, as_dict=1)
|
2019-11-29 12:02:17 +00:00
|
|
|
|
|
|
|
|
2019-06-14 06:19:20 +00:00
|
|
|
@frappe.whitelist()
|
2020-07-29 15:14:23 +00:00
|
|
|
def download_datev_csv(filters):
|
2019-06-14 06:19:20 +00:00
|
|
|
"""
|
|
|
|
Provide accounting entries for download in DATEV format.
|
|
|
|
|
|
|
|
Validate the filters, get the data, produce the CSV file and provide it for
|
|
|
|
download. Can be called like this:
|
|
|
|
|
|
|
|
GET /api/method/erpnext.regional.report.datev.datev.download_datev_csv
|
|
|
|
|
|
|
|
Arguments / Params:
|
|
|
|
filters -- dict of filters to be passed to the sql query
|
|
|
|
"""
|
|
|
|
if isinstance(filters, string_types):
|
|
|
|
filters = json.loads(filters)
|
|
|
|
|
2019-08-13 22:39:59 +00:00
|
|
|
validate(filters)
|
2020-11-05 15:57:23 +00:00
|
|
|
company = filters.get('company')
|
|
|
|
|
|
|
|
fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=company)
|
|
|
|
filters['fiscal_year_start'] = fiscal_year[1]
|
2019-06-14 06:19:20 +00:00
|
|
|
|
2020-05-11 16:50:02 +00:00
|
|
|
# set chart of accounts used
|
2020-11-05 15:57:23 +00:00
|
|
|
coa = frappe.get_value('Company', company, 'chart_of_accounts')
|
2020-05-11 16:50:02 +00:00
|
|
|
filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '')
|
|
|
|
|
2020-11-15 04:04:05 +00:00
|
|
|
filters['account_number_length'] = frappe.get_value('DATEV Settings', company, 'account_number_length')
|
|
|
|
|
2019-11-29 12:02:17 +00:00
|
|
|
transactions = get_transactions(filters)
|
|
|
|
account_names = get_account_names(filters)
|
|
|
|
customers = get_customers(filters)
|
|
|
|
suppliers = get_suppliers(filters)
|
2020-07-29 15:14:23 +00:00
|
|
|
|
2020-11-09 14:47:56 +00:00
|
|
|
zip_name = '{} DATEV.zip'.format(frappe.utils.datetime.date.today())
|
|
|
|
zip_and_download(zip_name, [
|
2020-07-29 15:14:23 +00:00
|
|
|
{
|
|
|
|
'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)
|
|
|
|
},
|
|
|
|
])
|