Merge branch 'develop' into values-out-of-sync-jv
diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml
index 8cc5826..cdb6849 100644
--- a/.github/workflows/server-tests-mariadb.yml
+++ b/.github/workflows/server-tests-mariadb.yml
@@ -129,6 +129,9 @@
needs: test
runs-on: ubuntu-latest
steps:
+ - name: Clone
+ uses: actions/checkout@v2
+
- name: Download artifacts
uses: actions/download-artifact@v3
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index 897151a..4453783 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -205,10 +205,16 @@
return frappe.get_hooks("accounting_dimension_doctypes")
-def get_accounting_dimensions(as_list=True):
+def get_accounting_dimensions(as_list=True, filters=None):
+
+ if not filters:
+ filters = {"disabled": 0}
+
if frappe.flags.accounting_dimensions is None:
frappe.flags.accounting_dimensions = frappe.get_all(
- "Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"]
+ "Accounting Dimension",
+ fields=["label", "fieldname", "disabled", "document_type"],
+ filters=filters,
)
if as_list:
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 907b769..b596df9 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -350,9 +350,13 @@
)
if self.minimum_invoice_amount:
- condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount))
+ condition += " and {dr_or_cr} >= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
+ )
if self.maximum_invoice_amount:
- condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount))
+ condition += " and {dr_or_cr} <= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
+ )
elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company)
@@ -367,15 +371,19 @@
else ""
)
dr_or_cr = (
- "gl.debit_in_account_currency"
+ "debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
- else "gl.credit_in_account_currency"
+ else "credit_in_account_currency"
)
if self.minimum_invoice_amount:
- condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount))
+ condition += " and gl.{dr_or_cr} >= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
+ )
if self.maximum_invoice_amount:
- condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount))
+ condition += " and gl.{dr_or_cr} <= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
+ )
else:
condition += (
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index e6a46d0..a5f9e24 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -811,7 +811,9 @@
if provisional_accounting_for_non_stock_items:
if item.purchase_receipt:
- provisional_account = self.get_company_default("default_provisional_account")
+ provisional_account = frappe.db.get_value(
+ "Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
+ ) or self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
@@ -834,7 +836,7 @@
if expense_booked_in_pr:
# Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(
- item, gl_entries, self.posting_date, reverse=1
+ item, gl_entries, self.posting_date, provisional_account, reverse=1
)
if not self.is_internal_transfer():
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 73390dd..59bd637 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -1482,7 +1482,8 @@
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
- item = create_item("_Test Non Stock Item", is_stock_item=0)
+ create_item("_Test Non Stock Item", is_stock_item=0)
+
provisional_account = create_account(
account_name="Provision Account",
parent_account="Current Liabilities - _TC",
@@ -1505,6 +1506,8 @@
pi.save()
pi.submit()
+ self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC")
+
# Check GLE for Purchase Invoice
expected_gle = [
["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)],
diff --git a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
index dcdba09..064b806 100644
--- a/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/test_request_for_quotation.py
@@ -65,7 +65,6 @@
)
sq.submit()
- frappe.form_dict = frappe.local("form_dict")
frappe.form_dict.name = rfq.name
self.assertEqual(check_supplier_has_docname_access(supplier_wt_appos[0].get("supplier")), True)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index eda3686..233b476 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -22,6 +22,9 @@
class BuyingController(StockController, Subcontracting):
+ def __setup__(self):
+ self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
+
def get_feed(self):
if self.get("supplier_name"):
return _("From {0} | {1} {2}").format(self.supplier_name, self.currency, self.grand_total)
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 7877827..19fedb3 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -16,6 +16,9 @@
class SellingController(StockController):
+ def __setup__(self):
+ self.flags.ignore_permlevel_for_fields = ["selling_price_list", "price_list_currency"]
+
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency, self.grand_total)
diff --git a/erpnext/erpnext_integrations/exotel_integration.py b/erpnext/erpnext_integrations/exotel_integration.py
index 1f5df67..522de9e 100644
--- a/erpnext/erpnext_integrations/exotel_integration.py
+++ b/erpnext/erpnext_integrations/exotel_integration.py
@@ -37,11 +37,26 @@
@frappe.whitelist(allow_guest=True)
def handle_missed_call(**kwargs):
- update_call_log(kwargs, "Missed")
+ status = ""
+ call_type = kwargs.get("CallType")
+ dial_call_status = kwargs.get("DialCallStatus")
+
+ if call_type == "incomplete" and dial_call_status == "no-answer":
+ status = "No Answer"
+ elif call_type == "client-hangup" and dial_call_status == "canceled":
+ status = "Canceled"
+ elif call_type == "incomplete" and dial_call_status == "failed":
+ status = "Failed"
+
+ update_call_log(kwargs, status)
def update_call_log(call_payload, status="Ringing", call_log=None):
call_log = call_log or get_call_log(call_payload)
+
+ # for a new sid, call_log and get_call_log will be empty so create a new log
+ if not call_log:
+ call_log = create_call_log(call_payload)
if call_log:
call_log.status = status
call_log.to = call_payload.get("DialWhomNumber")
@@ -53,16 +68,9 @@
def get_call_log(call_payload):
- call_log = frappe.get_all(
- "Call Log",
- {
- "id": call_payload.get("CallSid"),
- },
- limit=1,
- )
-
- if call_log:
- return frappe.get_doc("Call Log", call_log[0].name)
+ call_log_id = call_payload.get("CallSid")
+ if frappe.db.exists("Call Log", call_log_id):
+ return frappe.get_doc("Call Log", call_log_id)
def create_call_log(call_payload):
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index d592a9c..8a12f3b 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -4,7 +4,7 @@
"allow_import": 1,
"allow_rename": 1,
"autoname": "naming_series:",
- "creation": "2013-03-07 09:04:18",
+ "creation": "2022-02-21 11:54:09.632218",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
@@ -813,11 +813,12 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2021-06-17 11:31:37.730760",
+ "modified": "2022-03-22 13:44:37.088519",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -857,7 +858,9 @@
],
"search_fields": "employee_name",
"show_name_in_global_search": 1,
+ "show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "employee_name"
}
\ No newline at end of file
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js
index 035290d..5252798 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js
@@ -140,26 +140,6 @@
}
}
- start_date(doc, cdt, cdn) {
- this.set_no_of_visits(doc, cdt, cdn);
- }
-
- end_date(doc, cdt, cdn) {
- this.set_no_of_visits(doc, cdt, cdn);
- }
-
- periodicity(doc, cdt, cdn) {
- this.set_no_of_visits(doc, cdt, cdn);
- }
-
- set_no_of_visits(doc, cdt, cdn) {
- var item = frappe.get_doc(cdt, cdn);
- let me = this;
- if (item.start_date && item.periodicity) {
- me.frm.call('validate_end_date_visits');
-
- }
- }
};
extend_cscript(cur_frm.cscript, new erpnext.maintenance.MaintenanceSchedule({frm: cur_frm}));
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index 9a23c07..04c080c 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -213,6 +213,26 @@
if chk:
throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order))
+ def validate_items_table_change(self):
+ doc_before_save = self.get_doc_before_save()
+ if not doc_before_save:
+ return
+ for prev_item, item in zip(doc_before_save.items, self.items):
+ fields = [
+ "item_code",
+ "start_date",
+ "end_date",
+ "periodicity",
+ "sales_person",
+ "no_of_visits",
+ "serial_no",
+ ]
+ for field in fields:
+ b_doc = prev_item.as_dict()
+ doc = item.as_dict()
+ if cstr(b_doc[field]) != cstr(doc[field]):
+ return True
+
def validate_no_of_visits(self):
return len(self.schedules) != sum(d.no_of_visits for d in self.items)
@@ -221,7 +241,7 @@
self.validate_maintenance_detail()
self.validate_dates_with_periodicity()
self.validate_sales_order()
- if not self.schedules or self.validate_no_of_visits():
+ if not self.schedules or self.validate_items_table_change() or self.validate_no_of_visits():
self.generate_schedule()
def on_update(self):
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
index a98cd10..2268e0f 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/test_maintenance_schedule.py
@@ -123,6 +123,36 @@
frappe.db.rollback()
+ def test_schedule_with_serials(self):
+ # Checks whether serials are automatically updated when changing in items table.
+ # Also checks if other fields trigger generate schdeule if changed in items table.
+ item_code = "_Test Serial Item"
+ make_serial_item_with_serial(item_code)
+ ms = make_maintenance_schedule(item_code=item_code, serial_no="TEST001, TEST002")
+ ms.save()
+
+ # Before Save
+ self.assertEqual(ms.schedules[0].serial_no, "TEST001, TEST002")
+ self.assertEqual(ms.schedules[0].sales_person, "Sales Team")
+ self.assertEqual(len(ms.schedules), 4)
+ self.assertFalse(ms.validate_items_table_change())
+ # After Save
+ ms.items[0].serial_no = "TEST001"
+ ms.items[0].sales_person = "_Test Sales Person"
+ ms.items[0].no_of_visits = 2
+ self.assertTrue(ms.validate_items_table_change())
+ ms.save()
+ self.assertEqual(ms.schedules[0].serial_no, "TEST001")
+ self.assertEqual(ms.schedules[0].sales_person, "_Test Sales Person")
+ self.assertEqual(len(ms.schedules), 2)
+ # When user manually deleted a row from schedules table.
+ ms.schedules.pop()
+ self.assertEqual(len(ms.schedules), 1)
+ ms.save()
+ self.assertEqual(len(ms.schedules), 2)
+
+ frappe.db.rollback()
+
def make_serial_item_with_serial(item_code):
serial_item_doc = create_item(item_code, is_stock_item=1)
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 60b32b8..9ca05b9 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -462,6 +462,7 @@
work_order_data = {
"wip_warehouse": default_warehouses.get("wip_warehouse"),
"fg_warehouse": default_warehouses.get("fg_warehouse"),
+ "company": self.get("company"),
}
self.prepare_data_for_sub_assembly_items(row, work_order_data)
@@ -499,6 +500,7 @@
for supplier, po_list in subcontracted_po.items():
po = frappe.new_doc("Purchase Order")
+ po.company = self.company
po.supplier = supplier
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
po.is_subcontracted = 1
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 6e5ffed..63b6bb7 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -262,8 +262,6 @@
erpnext.patches.v13_0.update_shipment_status
erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
erpnext.patches.v12_0.add_ewaybill_validity_field
-erpnext.patches.v13_0.germany_make_custom_fields
-erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed
erpnext.patches.v13_0.rename_stop_to_send_birthday_reminders
execute:frappe.rename_doc("Workspace", "Loan Management", "Loans", force=True)
@@ -343,6 +341,7 @@
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
erpnext.patches.v14_0.delete_agriculture_doctypes
+erpnext.patches.v14_0.delete_datev_doctypes
erpnext.patches.v14_0.rearrange_company_fields
erpnext.patches.v14_0.update_leave_notification_template
erpnext.patches.v14_0.restore_einvoice_fields
@@ -364,4 +363,4 @@
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
-erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
\ No newline at end of file
+erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
diff --git a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py b/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py
deleted file mode 100644
index fc3e68a..0000000
--- a/erpnext/patches/v13_0/germany_fill_debtor_creditor_number.py
+++ /dev/null
@@ -1,36 +0,0 @@
-# Copyright (c) 2019, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-
-def execute():
- """Move account number into the new custom field debtor_creditor_number.
-
- German companies used to use a dedicated payable/receivable account for
- every party to mimick party accounts in the external accounting software
- "DATEV". This is no longer necessary. The reference ID for DATEV will be
- stored in a new custom field "debtor_creditor_number".
- """
- company_list = frappe.get_all("Company", filters={"country": "Germany"})
-
- for company in company_list:
- party_account_list = frappe.get_all(
- "Party Account",
- filters={"company": company.name},
- fields=["name", "account", "debtor_creditor_number"],
- )
- for party_account in party_account_list:
- if (not party_account.account) or party_account.debtor_creditor_number:
- # account empty or debtor_creditor_number already filled
- continue
-
- account_number = frappe.db.get_value("Account", party_account.account, "account_number")
- if not account_number:
- continue
-
- frappe.db.set_value(
- "Party Account", party_account.name, "debtor_creditor_number", account_number
- )
- frappe.db.set_value("Party Account", party_account.name, "account", "")
diff --git a/erpnext/patches/v13_0/germany_make_custom_fields.py b/erpnext/patches/v13_0/germany_make_custom_fields.py
deleted file mode 100644
index cc35813..0000000
--- a/erpnext/patches/v13_0/germany_make_custom_fields.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Copyright (c) 2019, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-
-import frappe
-
-from erpnext.regional.germany.setup import make_custom_fields
-
-
-def execute():
- """Execute the make_custom_fields method for german companies.
-
- It is usually run once at setup of a new company. Since it's new, run it
- once for existing companies as well.
- """
- company_list = frappe.get_all("Company", filters={"country": "Germany"})
- if not company_list:
- return
-
- make_custom_fields()
diff --git a/erpnext/patches/v14_0/delete_datev_doctypes.py b/erpnext/patches/v14_0/delete_datev_doctypes.py
new file mode 100644
index 0000000..a5de91f
--- /dev/null
+++ b/erpnext/patches/v14_0/delete_datev_doctypes.py
@@ -0,0 +1,13 @@
+import frappe
+
+
+def execute():
+ install_apps = frappe.get_installed_apps()
+ if "erpnext_datev_uo" in install_apps or "erpnext_datev" in install_apps:
+ return
+
+ # doctypes
+ frappe.delete_doc("DocType", "DATEV Settings", ignore_missing=True, force=True)
+
+ # reports
+ frappe.delete_doc("Report", "DATEV", ignore_missing=True, force=True)
diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js
index c954f12..2dbe999 100644
--- a/erpnext/public/js/call_popup/call_popup.js
+++ b/erpnext/public/js/call_popup/call_popup.js
@@ -141,6 +141,14 @@
'fieldtype': 'Section Break',
'hide_border': 1,
}, {
+ 'fieldname': 'call_type',
+ 'label': 'Call Type',
+ 'fieldtype': 'Link',
+ 'options': 'Telephony Call Type',
+ }, {
+ 'fieldtype': 'Section Break',
+ 'hide_border': 1,
+ }, {
'fieldtype': 'Small Text',
'label': __('Call Summary'),
'fieldname': 'call_summary',
@@ -149,10 +157,12 @@
'label': __('Save'),
'click': () => {
const call_summary = this.call_details.get_value('call_summary');
+ const call_type = this.call_details.get_value('call_type');
if (!call_summary) return;
- frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', {
+ frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary_and_call_type', {
'call_log': this.call_log.name,
'summary': call_summary,
+ 'call_type': call_type,
}).then(() => {
this.close_modal();
frappe.show_alert({
diff --git a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
index daf7a69..04e8211 100644
--- a/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
+++ b/erpnext/quality_management/doctype/quality_procedure/test_quality_procedure.py
@@ -19,7 +19,7 @@
)
).insert()
- frappe.form_dict = dict(
+ frappe.local.form_dict = frappe._dict(
doctype="Quality Procedure",
quality_procedure_name="Test Child 1",
parent_quality_procedure=procedure.name,
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.js b/erpnext/regional/doctype/datev_settings/datev_settings.js
deleted file mode 100644
index 3c36549..0000000
--- a/erpnext/regional/doctype/datev_settings/datev_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('DATEV Settings', {
- refresh: function(frm) {
- frm.add_custom_button(__('Show Report'), () => frappe.set_route('query-report', 'DATEV'), "fa fa-table");
- }
-});
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json
deleted file mode 100644
index f60de4c..0000000
--- a/erpnext/regional/doctype/datev_settings/datev_settings.json
+++ /dev/null
@@ -1,125 +0,0 @@
-{
- "actions": [],
- "autoname": "field:client",
- "creation": "2019-08-13 23:56:34.259906",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "client",
- "client_number",
- "column_break_2",
- "consultant_number",
- "consultant",
- "section_break_4",
- "account_number_length",
- "column_break_6",
- "temporary_against_account_number"
- ],
- "fields": [
- {
- "fieldname": "client",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Client",
- "options": "Company",
- "reqd": 1,
- "unique": 1
- },
- {
- "fieldname": "client_number",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Client ID",
- "length": 5,
- "reqd": 1
- },
- {
- "fieldname": "consultant",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "Consultant",
- "options": "Supplier"
- },
- {
- "fieldname": "consultant_number",
- "fieldtype": "Data",
- "in_list_view": 1,
- "label": "Consultant ID",
- "length": 7,
- "reqd": 1
- },
- {
- "fieldname": "column_break_2",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "section_break_4",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Column Break"
- },
- {
- "default": "4",
- "fieldname": "account_number_length",
- "fieldtype": "Int",
- "label": "Account Number Length",
- "reqd": 1
- },
- {
- "allow_in_quick_entry": 1,
- "fieldname": "temporary_against_account_number",
- "fieldtype": "Data",
- "label": "Temporary Against Account Number",
- "reqd": 1
- }
- ],
- "links": [],
- "modified": "2020-11-19 19:00:09.088816",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "DATEV Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "share": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.py b/erpnext/regional/doctype/datev_settings/datev_settings.py
deleted file mode 100644
index 686a93e..0000000
--- a/erpnext/regional/doctype/datev_settings/datev_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-
-# import frappe
-from frappe.model.document import Document
-
-
-class DATEVSettings(Document):
- pass
diff --git a/erpnext/regional/doctype/datev_settings/test_datev_settings.py b/erpnext/regional/doctype/datev_settings/test_datev_settings.py
deleted file mode 100644
index ba70eb4..0000000
--- a/erpnext/regional/doctype/datev_settings/test_datev_settings.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
-# See license.txt
-
-# import frappe
-import unittest
-
-
-class TestDATEVSettings(unittest.TestCase):
- pass
diff --git a/erpnext/regional/germany/__init__.py b/erpnext/regional/germany/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/germany/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/germany/setup.py b/erpnext/regional/germany/setup.py
deleted file mode 100644
index b8e66c3..0000000
--- a/erpnext/regional/germany/setup.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import frappe
-from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-
-
-def setup(company=None, patch=True):
- make_custom_fields()
- add_custom_roles_for_reports()
-
-
-def make_custom_fields():
- custom_fields = {
- "Party Account": [
- dict(
- fieldname="debtor_creditor_number",
- label="Debtor/Creditor Number",
- fieldtype="Data",
- insert_after="account",
- translatable=0,
- )
- ]
- }
-
- create_custom_fields(custom_fields)
-
-
-def add_custom_roles_for_reports():
- """Add Access Control to UAE VAT 201."""
- if not frappe.db.get_value("Custom Role", dict(report="DATEV")):
- frappe.get_doc(
- dict(
- doctype="Custom Role",
- report="DATEV",
- roles=[dict(role="Accounts User"), dict(role="Accounts Manager")],
- )
- ).insert()
diff --git a/erpnext/regional/germany/utils/__init__.py b/erpnext/regional/germany/utils/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/germany/utils/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/germany/utils/datev/__init__.py b/erpnext/regional/germany/utils/datev/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/germany/utils/datev/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/germany/utils/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py
deleted file mode 100644
index 9524481..0000000
--- a/erpnext/regional/germany/utils/datev/datev_constants.py
+++ /dev/null
@@ -1,501 +0,0 @@
-"""Constants used in datev.py."""
-
-TRANSACTION_COLUMNS = [
- # All possible columns must tbe listed here, because DATEV requires them to
- # be present in the CSV.
- # ---
- # Umsatz
- "Umsatz (ohne Soll/Haben-Kz)",
- "Soll/Haben-Kennzeichen",
- "WKZ Umsatz",
- "Kurs",
- "Basis-Umsatz",
- "WKZ Basis-Umsatz",
- # Konto/Gegenkonto
- "Konto",
- "Gegenkonto (ohne BU-Schlüssel)",
- "BU-Schlüssel",
- # Datum
- "Belegdatum",
- # Rechnungs- / Belegnummer
- "Belegfeld 1",
- # z.B. Fälligkeitsdatum Format: TTMMJJ
- "Belegfeld 2",
- # Skonto-Betrag / -Abzug (Der Wert 0 ist unzulässig)
- "Skonto",
- # Beschreibung des Buchungssatzes
- "Buchungstext",
- # Mahn- / Zahl-Sperre (1 = Postensperre)
- "Postensperre",
- "Diverse Adressnummer",
- "Geschäftspartnerbank",
- "Sachverhalt",
- # Keine Mahnzinsen
- "Zinssperre",
- # Link auf den Buchungsbeleg (Programmkürzel + GUID)
- "Beleglink",
- # Beleginfo
- "Beleginfo - Art 1",
- "Beleginfo - Inhalt 1",
- "Beleginfo - Art 2",
- "Beleginfo - Inhalt 2",
- "Beleginfo - Art 3",
- "Beleginfo - Inhalt 3",
- "Beleginfo - Art 4",
- "Beleginfo - Inhalt 4",
- "Beleginfo - Art 5",
- "Beleginfo - Inhalt 5",
- "Beleginfo - Art 6",
- "Beleginfo - Inhalt 6",
- "Beleginfo - Art 7",
- "Beleginfo - Inhalt 7",
- "Beleginfo - Art 8",
- "Beleginfo - Inhalt 8",
- # Zuordnung des Geschäftsvorfalls für die Kostenrechnung
- "KOST1 - Kostenstelle",
- "KOST2 - Kostenstelle",
- "KOST-Menge",
- # USt-ID-Nummer (Beispiel: DE133546770)
- "EU-Mitgliedstaat u. USt-IdNr.",
- # Der im EU-Bestimmungsland gültige Steuersatz
- "EU-Steuersatz",
- # I = Ist-Versteuerung,
- # K = keine Umsatzsteuerrechnung
- # P = Pauschalierung (z. B. für Land- und Forstwirtschaft),
- # S = Soll-Versteuerung
- "Abw. Versteuerungsart",
- # Sachverhalte gem. § 13b Abs. 1 Satz 1 Nrn. 1.-5. UStG
- "Sachverhalt L+L",
- # Steuersatz / Funktion zum L+L-Sachverhalt (Beispiel: Wert 190 für 19%)
- "Funktionsergänzung L+L",
- # Bei Verwendung des BU-Schlüssels 49 für „andere Steuersätze“ muss der
- # steuerliche Sachverhalt mitgegeben werden
- "BU 49 Hauptfunktionstyp",
- "BU 49 Hauptfunktionsnummer",
- "BU 49 Funktionsergänzung",
- # Zusatzinformationen, besitzen den Charakter eines Notizzettels und können
- # frei erfasst werden.
- "Zusatzinformation - Art 1",
- "Zusatzinformation - Inhalt 1",
- "Zusatzinformation - Art 2",
- "Zusatzinformation - Inhalt 2",
- "Zusatzinformation - Art 3",
- "Zusatzinformation - Inhalt 3",
- "Zusatzinformation - Art 4",
- "Zusatzinformation - Inhalt 4",
- "Zusatzinformation - Art 5",
- "Zusatzinformation - Inhalt 5",
- "Zusatzinformation - Art 6",
- "Zusatzinformation - Inhalt 6",
- "Zusatzinformation - Art 7",
- "Zusatzinformation - Inhalt 7",
- "Zusatzinformation - Art 8",
- "Zusatzinformation - Inhalt 8",
- "Zusatzinformation - Art 9",
- "Zusatzinformation - Inhalt 9",
- "Zusatzinformation - Art 10",
- "Zusatzinformation - Inhalt 10",
- "Zusatzinformation - Art 11",
- "Zusatzinformation - Inhalt 11",
- "Zusatzinformation - Art 12",
- "Zusatzinformation - Inhalt 12",
- "Zusatzinformation - Art 13",
- "Zusatzinformation - Inhalt 13",
- "Zusatzinformation - Art 14",
- "Zusatzinformation - Inhalt 14",
- "Zusatzinformation - Art 15",
- "Zusatzinformation - Inhalt 15",
- "Zusatzinformation - Art 16",
- "Zusatzinformation - Inhalt 16",
- "Zusatzinformation - Art 17",
- "Zusatzinformation - Inhalt 17",
- "Zusatzinformation - Art 18",
- "Zusatzinformation - Inhalt 18",
- "Zusatzinformation - Art 19",
- "Zusatzinformation - Inhalt 19",
- "Zusatzinformation - Art 20",
- "Zusatzinformation - Inhalt 20",
- # Wirkt sich nur bei Sachverhalt mit SKR 14 Land- und Forstwirtschaft aus,
- # für andere SKR werden die Felder beim Import / Export überlesen bzw.
- # leer exportiert.
- "Stück",
- "Gewicht",
- # 1 = Lastschrift
- # 2 = Mahnung
- # 3 = Zahlung
- "Zahlweise",
- "Forderungsart",
- # JJJJ
- "Veranlagungsjahr",
- # TTMMJJJJ
- "Zugeordnete Fälligkeit",
- # 1 = Einkauf von Waren
- # 2 = Erwerb von Roh-Hilfs- und Betriebsstoffen
- "Skontotyp",
- # Allgemeine Bezeichnung, des Auftrags / Projekts.
- "Auftragsnummer",
- # AA = Angeforderte Anzahlung / Abschlagsrechnung
- # AG = Erhaltene Anzahlung (Geldeingang)
- # AV = Erhaltene Anzahlung (Verbindlichkeit)
- # SR = Schlussrechnung
- # SU = Schlussrechnung (Umbuchung)
- # SG = Schlussrechnung (Geldeingang)
- # SO = Sonstige
- "Buchungstyp",
- "USt-Schlüssel (Anzahlungen)",
- "EU-Mitgliedstaat (Anzahlungen)",
- "Sachverhalt L+L (Anzahlungen)",
- "EU-Steuersatz (Anzahlungen)",
- "Erlöskonto (Anzahlungen)",
- # Wird beim Import durch SV (Stapelverarbeitung) ersetzt.
- "Herkunft-Kz",
- # Wird von DATEV verwendet.
- "Leerfeld",
- # Format TTMMJJJJ
- "KOST-Datum",
- # Vom Zahlungsempfänger individuell vergebenes Kennzeichen eines Mandats
- # (z.B. Rechnungs- oder Kundennummer).
- "SEPA-Mandatsreferenz",
- # 1 = Skontosperre
- # 0 = Keine Skontosperre
- "Skontosperre",
- # Gesellschafter und Sonderbilanzsachverhalt
- "Gesellschaftername",
- # Amtliche Nummer aus der Feststellungserklärung
- "Beteiligtennummer",
- "Identifikationsnummer",
- "Zeichnernummer",
- # Format TTMMJJJJ
- "Postensperre bis",
- # Gesellschafter und Sonderbilanzsachverhalt
- "Bezeichnung SoBil-Sachverhalt",
- "Kennzeichen SoBil-Buchung",
- # 0 = keine Festschreibung
- # 1 = Festschreibung
- "Festschreibung",
- # Format TTMMJJJJ
- "Leistungsdatum",
- # Format TTMMJJJJ
- "Datum Zuord. Steuerperiode",
- # OPOS-Informationen, Format TTMMJJJJ
- "Fälligkeit",
- # G oder 1 = Generalumkehr
- # 0 = keine Generalumkehr
- "Generalumkehr (GU)",
- # Steuersatz für Steuerschlüssel
- "Steuersatz",
- # Beispiel: DE für Deutschland
- "Land",
-]
-
-DEBTOR_CREDITOR_COLUMNS = [
- # All possible columns must tbe listed here, because DATEV requires them to
- # be present in the CSV.
- # Columns "Leerfeld" have been replaced with "Leerfeld #" to not confuse pandas
- # ---
- "Konto",
- "Name (Adressatentyp Unternehmen)",
- "Unternehmensgegenstand",
- "Name (Adressatentyp natürl. Person)",
- "Vorname (Adressatentyp natürl. Person)",
- "Name (Adressatentyp keine Angabe)",
- "Adressatentyp",
- "Kurzbezeichnung",
- "EU-Land",
- "EU-USt-IdNr.",
- "Anrede",
- "Titel/Akad. Grad",
- "Adelstitel",
- "Namensvorsatz",
- "Adressart",
- "Straße",
- "Postfach",
- "Postleitzahl",
- "Ort",
- "Land",
- "Versandzusatz",
- "Adresszusatz",
- "Abweichende Anrede",
- "Abw. Zustellbezeichnung 1",
- "Abw. Zustellbezeichnung 2",
- "Kennz. Korrespondenzadresse",
- "Adresse gültig von",
- "Adresse gültig bis",
- "Telefon",
- "Bemerkung (Telefon)",
- "Telefon Geschäftsleitung",
- "Bemerkung (Telefon GL)",
- "E-Mail",
- "Bemerkung (E-Mail)",
- "Internet",
- "Bemerkung (Internet)",
- "Fax",
- "Bemerkung (Fax)",
- "Sonstige",
- "Bemerkung (Sonstige)",
- "Bankleitzahl 1",
- "Bankbezeichnung 1",
- "Bankkonto-Nummer 1",
- "Länderkennzeichen 1",
- "IBAN 1",
- "Leerfeld 1",
- "SWIFT-Code 1",
- "Abw. Kontoinhaber 1",
- "Kennz. Haupt-Bankverb. 1",
- "Bankverb. 1 Gültig von",
- "Bankverb. 1 Gültig bis",
- "Bankleitzahl 2",
- "Bankbezeichnung 2",
- "Bankkonto-Nummer 2",
- "Länderkennzeichen 2",
- "IBAN 2",
- "Leerfeld 2",
- "SWIFT-Code 2",
- "Abw. Kontoinhaber 2",
- "Kennz. Haupt-Bankverb. 2",
- "Bankverb. 2 gültig von",
- "Bankverb. 2 gültig bis",
- "Bankleitzahl 3",
- "Bankbezeichnung 3",
- "Bankkonto-Nummer 3",
- "Länderkennzeichen 3",
- "IBAN 3",
- "Leerfeld 3",
- "SWIFT-Code 3",
- "Abw. Kontoinhaber 3",
- "Kennz. Haupt-Bankverb. 3",
- "Bankverb. 3 gültig von",
- "Bankverb. 3 gültig bis",
- "Bankleitzahl 4",
- "Bankbezeichnung 4",
- "Bankkonto-Nummer 4",
- "Länderkennzeichen 4",
- "IBAN 4",
- "Leerfeld 4",
- "SWIFT-Code 4",
- "Abw. Kontoinhaber 4",
- "Kennz. Haupt-Bankverb. 4",
- "Bankverb. 4 Gültig von",
- "Bankverb. 4 Gültig bis",
- "Bankleitzahl 5",
- "Bankbezeichnung 5",
- "Bankkonto-Nummer 5",
- "Länderkennzeichen 5",
- "IBAN 5",
- "Leerfeld 5",
- "SWIFT-Code 5",
- "Abw. Kontoinhaber 5",
- "Kennz. Haupt-Bankverb. 5",
- "Bankverb. 5 gültig von",
- "Bankverb. 5 gültig bis",
- "Leerfeld 6",
- "Briefanrede",
- "Grußformel",
- "Kundennummer",
- "Steuernummer",
- "Sprache",
- "Ansprechpartner",
- "Vertreter",
- "Sachbearbeiter",
- "Diverse-Konto",
- "Ausgabeziel",
- "Währungssteuerung",
- "Kreditlimit (Debitor)",
- "Zahlungsbedingung",
- "Fälligkeit in Tagen (Debitor)",
- "Skonto in Prozent (Debitor)",
- "Kreditoren-Ziel 1 (Tage)",
- "Kreditoren-Skonto 1 (%)",
- "Kreditoren-Ziel 2 (Tage)",
- "Kreditoren-Skonto 2 (%)",
- "Kreditoren-Ziel 3 Brutto (Tage)",
- "Kreditoren-Ziel 4 (Tage)",
- "Kreditoren-Skonto 4 (%)",
- "Kreditoren-Ziel 5 (Tage)",
- "Kreditoren-Skonto 5 (%)",
- "Mahnung",
- "Kontoauszug",
- "Mahntext 1",
- "Mahntext 2",
- "Mahntext 3",
- "Kontoauszugstext",
- "Mahnlimit Betrag",
- "Mahnlimit %",
- "Zinsberechnung",
- "Mahnzinssatz 1",
- "Mahnzinssatz 2",
- "Mahnzinssatz 3",
- "Lastschrift",
- "Verfahren",
- "Mandantenbank",
- "Zahlungsträger",
- "Indiv. Feld 1",
- "Indiv. Feld 2",
- "Indiv. Feld 3",
- "Indiv. Feld 4",
- "Indiv. Feld 5",
- "Indiv. Feld 6",
- "Indiv. Feld 7",
- "Indiv. Feld 8",
- "Indiv. Feld 9",
- "Indiv. Feld 10",
- "Indiv. Feld 11",
- "Indiv. Feld 12",
- "Indiv. Feld 13",
- "Indiv. Feld 14",
- "Indiv. Feld 15",
- "Abweichende Anrede (Rechnungsadresse)",
- "Adressart (Rechnungsadresse)",
- "Straße (Rechnungsadresse)",
- "Postfach (Rechnungsadresse)",
- "Postleitzahl (Rechnungsadresse)",
- "Ort (Rechnungsadresse)",
- "Land (Rechnungsadresse)",
- "Versandzusatz (Rechnungsadresse)",
- "Adresszusatz (Rechnungsadresse)",
- "Abw. Zustellbezeichnung 1 (Rechnungsadresse)",
- "Abw. Zustellbezeichnung 2 (Rechnungsadresse)",
- "Adresse Gültig von (Rechnungsadresse)",
- "Adresse Gültig bis (Rechnungsadresse)",
- "Bankleitzahl 6",
- "Bankbezeichnung 6",
- "Bankkonto-Nummer 6",
- "Länderkennzeichen 6",
- "IBAN 6",
- "Leerfeld 7",
- "SWIFT-Code 6",
- "Abw. Kontoinhaber 6",
- "Kennz. Haupt-Bankverb. 6",
- "Bankverb 6 gültig von",
- "Bankverb 6 gültig bis",
- "Bankleitzahl 7",
- "Bankbezeichnung 7",
- "Bankkonto-Nummer 7",
- "Länderkennzeichen 7",
- "IBAN 7",
- "Leerfeld 8",
- "SWIFT-Code 7",
- "Abw. Kontoinhaber 7",
- "Kennz. Haupt-Bankverb. 7",
- "Bankverb 7 gültig von",
- "Bankverb 7 gültig bis",
- "Bankleitzahl 8",
- "Bankbezeichnung 8",
- "Bankkonto-Nummer 8",
- "Länderkennzeichen 8",
- "IBAN 8",
- "Leerfeld 9",
- "SWIFT-Code 8",
- "Abw. Kontoinhaber 8",
- "Kennz. Haupt-Bankverb. 8",
- "Bankverb 8 gültig von",
- "Bankverb 8 gültig bis",
- "Bankleitzahl 9",
- "Bankbezeichnung 9",
- "Bankkonto-Nummer 9",
- "Länderkennzeichen 9",
- "IBAN 9",
- "Leerfeld 10",
- "SWIFT-Code 9",
- "Abw. Kontoinhaber 9",
- "Kennz. Haupt-Bankverb. 9",
- "Bankverb 9 gültig von",
- "Bankverb 9 gültig bis",
- "Bankleitzahl 10",
- "Bankbezeichnung 10",
- "Bankkonto-Nummer 10",
- "Länderkennzeichen 10",
- "IBAN 10",
- "Leerfeld 11",
- "SWIFT-Code 10",
- "Abw. Kontoinhaber 10",
- "Kennz. Haupt-Bankverb. 10",
- "Bankverb 10 gültig von",
- "Bankverb 10 gültig bis",
- "Nummer Fremdsystem",
- "Insolvent",
- "SEPA-Mandatsreferenz 1",
- "SEPA-Mandatsreferenz 2",
- "SEPA-Mandatsreferenz 3",
- "SEPA-Mandatsreferenz 4",
- "SEPA-Mandatsreferenz 5",
- "SEPA-Mandatsreferenz 6",
- "SEPA-Mandatsreferenz 7",
- "SEPA-Mandatsreferenz 8",
- "SEPA-Mandatsreferenz 9",
- "SEPA-Mandatsreferenz 10",
- "Verknüpftes OPOS-Konto",
- "Mahnsperre bis",
- "Lastschriftsperre bis",
- "Zahlungssperre bis",
- "Gebührenberechnung",
- "Mahngebühr 1",
- "Mahngebühr 2",
- "Mahngebühr 3",
- "Pauschalberechnung",
- "Verzugspauschale 1",
- "Verzugspauschale 2",
- "Verzugspauschale 3",
- "Alternativer Suchname",
- "Status",
- "Anschrift manuell geändert (Korrespondenzadresse)",
- "Anschrift individuell (Korrespondenzadresse)",
- "Anschrift manuell geändert (Rechnungsadresse)",
- "Anschrift individuell (Rechnungsadresse)",
- "Fristberechnung bei Debitor",
- "Mahnfrist 1",
- "Mahnfrist 2",
- "Mahnfrist 3",
- "Letzte Frist",
-]
-
-ACCOUNT_NAME_COLUMNS = [
- # Account number
- "Konto",
- # Account name
- "Kontenbeschriftung",
- # Language of the account name
- # "de-DE" or "en-GB"
- "Sprach-ID",
-]
-
-
-class DataCategory:
-
- """Field of the CSV Header."""
-
- DEBTORS_CREDITORS = "16"
- ACCOUNT_NAMES = "20"
- TRANSACTIONS = "21"
- POSTING_TEXT_CONSTANTS = "67"
-
-
-class FormatName:
-
- """Field of the CSV Header, corresponds to DataCategory."""
-
- DEBTORS_CREDITORS = "Debitoren/Kreditoren"
- ACCOUNT_NAMES = "Kontenbeschriftungen"
- TRANSACTIONS = "Buchungsstapel"
- POSTING_TEXT_CONSTANTS = "Buchungstextkonstanten"
-
-
-class Transactions:
- DATA_CATEGORY = DataCategory.TRANSACTIONS
- FORMAT_NAME = FormatName.TRANSACTIONS
- FORMAT_VERSION = "9"
- COLUMNS = TRANSACTION_COLUMNS
-
-
-class DebtorsCreditors:
- DATA_CATEGORY = DataCategory.DEBTORS_CREDITORS
- FORMAT_NAME = FormatName.DEBTORS_CREDITORS
- FORMAT_VERSION = "5"
- COLUMNS = DEBTOR_CREDITOR_COLUMNS
-
-
-class AccountNames:
- DATA_CATEGORY = DataCategory.ACCOUNT_NAMES
- FORMAT_NAME = FormatName.ACCOUNT_NAMES
- FORMAT_VERSION = "2"
- COLUMNS = ACCOUNT_NAME_COLUMNS
diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py
deleted file mode 100644
index d4e9c27..0000000
--- a/erpnext/regional/germany/utils/datev/datev_csv.py
+++ /dev/null
@@ -1,184 +0,0 @@
-import datetime
-import zipfile
-from csv import QUOTE_NONNUMERIC
-from io import BytesIO
-
-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"])
-
- result["Beleginfo - Inhalt 6"] = pd.to_datetime(result["Beleginfo - Inhalt 6"])
- result["Beleginfo - Inhalt 6"] = result["Beleginfo - Inhalt 6"].dt.strftime("%d%m%Y")
-
- result["Fälligkeit"] = pd.to_datetime(result["Fälligkeit"])
- result["Fälligkeit"] = result["Fälligkeit"].dt.strftime("%d%m%y")
-
- result.sort_values(by="Belegdatum", inplace=True, kind="stable", ignore_index=True)
-
- 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=";",
- # 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,
- )
-
- data = data.encode("latin_1", errors="replace")
-
- header = get_header(filters, csv_class)
- header = ";".join(header).encode("latin_1", errors="replace")
-
- # 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(filters.get("fiscal_year_start"), "yyyyMMdd"),
- # N = Length of account numbers (Sachkontenlänge)
- str(filters.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 zip_and_download(zip_filename, csv_files):
- """
- Put CSV files in a zip archive and send that to the client.
-
- Params:
- zip_filename Name of the zip file
- csv_files list of dicts [{'file_name': 'my_file.csv', 'csv_data': 'comma,separated,values'}]
- """
- zip_buffer = BytesIO()
-
- zip_file = zipfile.ZipFile(zip_buffer, mode="w", compression=zipfile.ZIP_DEFLATED)
- for csv_file in csv_files:
- zip_file.writestr(csv_file.get("file_name"), csv_file.get("csv_data"))
-
- zip_file.close()
-
- frappe.response["filecontent"] = zip_buffer.getvalue()
- frappe.response["filename"] = zip_filename
- frappe.response["type"] = "binary"
diff --git a/erpnext/regional/report/datev/__init__.py b/erpnext/regional/report/datev/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/regional/report/datev/__init__.py
+++ /dev/null
diff --git a/erpnext/regional/report/datev/datev.js b/erpnext/regional/report/datev/datev.js
deleted file mode 100644
index 03c729e..0000000
--- a/erpnext/regional/report/datev/datev.js
+++ /dev/null
@@ -1,56 +0,0 @@
-frappe.query_reports["DATEV"] = {
- "filters": [
- {
- "fieldname": "company",
- "label": __("Company"),
- "fieldtype": "Link",
- "options": "Company",
- "default": frappe.defaults.get_user_default("Company") || frappe.defaults.get_global_default("Company"),
- "reqd": 1
- },
- {
- "fieldname": "from_date",
- "label": __("From Date"),
- "default": moment().subtract(1, 'month').startOf('month').format(),
- "fieldtype": "Date",
- "reqd": 1
- },
- {
- "fieldname": "to_date",
- "label": __("To Date"),
- "default": moment().subtract(1, 'month').endOf('month').format(),
- "fieldtype": "Date",
- "reqd": 1
- },
- {
- "fieldname": "voucher_type",
- "label": __("Voucher Type"),
- "fieldtype": "Select",
- "options": "\nSales Invoice\nPurchase Invoice\nPayment Entry\nExpense Claim\nPayroll Entry\nBank Reconciliation\nAsset\nStock Entry"
- }
- ],
- onload: function(query_report) {
- let company = frappe.query_report.get_filter_value('company');
- frappe.db.exists('DATEV Settings', company).then((settings_exist) => {
- if (!settings_exist) {
- frappe.confirm(__('DATEV Settings for your Company are missing. Would you like to create them now?'),
- () => frappe.new_doc('DATEV Settings', {'company': company})
- );
- }
- });
-
- query_report.page.add_menu_item(__("Download DATEV File"), () => {
- const filters = encodeURIComponent(
- JSON.stringify(
- query_report.get_values()
- )
- );
- window.open(`/api/method/erpnext.regional.report.datev.datev.download_datev_csv?filters=${filters}`);
- });
-
- query_report.page.add_menu_item(__("Change DATEV Settings"), () => {
- let company = frappe.query_report.get_filter_value('company'); // read company from filters again – it might have changed by now.
- frappe.set_route('Form', 'DATEV Settings', company);
- });
- }
-};
diff --git a/erpnext/regional/report/datev/datev.json b/erpnext/regional/report/datev/datev.json
deleted file mode 100644
index 94e3960..0000000
--- a/erpnext/regional/report/datev/datev.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "add_total_row": 0,
- "columns": [],
- "creation": "2019-04-24 08:45:16.650129",
- "disable_prepared_report": 0,
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "filters": [],
- "idx": 0,
- "is_standard": "Yes",
- "modified": "2021-04-06 12:23:00.379517",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "DATEV",
- "owner": "Administrator",
- "prepared_report": 0,
- "ref_doctype": "GL Entry",
- "report_name": "DATEV",
- "report_type": "Script Report",
- "roles": []
-}
\ No newline at end of file
diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
deleted file mode 100644
index 2d888a8..0000000
--- a/erpnext/regional/report/datev/datev.py
+++ /dev/null
@@ -1,570 +0,0 @@
-"""
-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.
-"""
-
-import json
-
-import frappe
-from frappe import _
-
-from erpnext.accounts.utils import get_fiscal_year
-from erpnext.regional.germany.utils.datev.datev_constants import (
- AccountNames,
- DebtorsCreditors,
- Transactions,
-)
-from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, zip_and_download
-
-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": "BU-Schlüssel", "fieldname": "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,
- },
- {
- "label": "Beleginfo - Art 3",
- "fieldname": "Beleginfo - Art 3",
- "fieldtype": "Link",
- "options": "DocType",
- "width": 100,
- },
- {
- "label": "Beleginfo - Inhalt 3",
- "fieldname": "Beleginfo - Inhalt 3",
- "fieldtype": "Dynamic Link",
- "options": "Beleginfo - Art 3",
- "width": 150,
- },
- {
- "label": "Beleginfo - Art 4",
- "fieldname": "Beleginfo - Art 4",
- "fieldtype": "Data",
- "width": 100,
- },
- {
- "label": "Beleginfo - Inhalt 4",
- "fieldname": "Beleginfo - Inhalt 4",
- "fieldtype": "Data",
- "width": 150,
- },
- {
- "label": "Beleginfo - Art 5",
- "fieldname": "Beleginfo - Art 5",
- "fieldtype": "Data",
- "width": 150,
- },
- {
- "label": "Beleginfo - Inhalt 5",
- "fieldname": "Beleginfo - Inhalt 5",
- "fieldtype": "Data",
- "width": 100,
- },
- {
- "label": "Beleginfo - Art 6",
- "fieldname": "Beleginfo - Art 6",
- "fieldtype": "Data",
- "width": 150,
- },
- {
- "label": "Beleginfo - Inhalt 6",
- "fieldname": "Beleginfo - Inhalt 6",
- "fieldtype": "Date",
- "width": 100,
- },
- {"label": "Fälligkeit", "fieldname": "Fälligkeit", "fieldtype": "Date", "width": 100},
-]
-
-
-def execute(filters=None):
- """Entry point for frappe."""
- data = []
- if filters and validate(filters):
- fn = "temporary_against_account_number"
- filters[fn] = frappe.get_value("DATEV Settings", filters.get("company"), fn)
- data = get_transactions(filters, as_dict=0)
-
- return COLUMNS, data
-
-
-def validate(filters):
- """Make sure all mandatory filters and settings are present."""
- company = filters.get("company")
- if not company:
- frappe.throw(_("<b>Company</b> is a mandatory filter."))
-
- from_date = filters.get("from_date")
- if not from_date:
- frappe.throw(_("<b>From Date</b> is a mandatory filter."))
-
- to_date = filters.get("to_date")
- if not to_date:
- frappe.throw(_("<b>To Date</b> is a mandatory filter."))
-
- validate_fiscal_year(from_date, to_date, company)
-
- if not frappe.db.exists("DATEV Settings", filters.get("company")):
- msg = "Please create DATEV Settings for Company {}".format(filters.get("company"))
- frappe.log_error(msg, title="DATEV Settings missing")
- return False
-
- return True
-
-
-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))
-
-
-def get_transactions(filters, as_dict=1):
- def run(params_method, filters):
- extra_fields, extra_joins, extra_filters = params_method(filters)
- return run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=as_dict)
-
- def sort_by(row):
- # "Belegdatum" is in the fifth column when list format is used
- return row["Belegdatum" if as_dict else 5]
-
- type_map = {
- # specific query methods for some voucher types
- "Payment Entry": get_payment_entry_params,
- "Sales Invoice": get_sales_invoice_params,
- "Purchase Invoice": get_purchase_invoice_params,
- }
-
- only_voucher_type = filters.get("voucher_type")
- transactions = []
-
- for voucher_type, get_voucher_params in type_map.items():
- if only_voucher_type and only_voucher_type != voucher_type:
- continue
-
- transactions.extend(run(params_method=get_voucher_params, filters=filters))
-
- if not only_voucher_type or only_voucher_type not in type_map:
- # generic query method for all other voucher types
- filters["exclude_voucher_types"] = type_map.keys()
- transactions.extend(run(params_method=get_generic_params, filters=filters))
-
- return sorted(transactions, key=sort_by)
-
-
-def get_payment_entry_params(filters):
- extra_fields = """
- , 'Zahlungsreferenz' as 'Beleginfo - Art 5'
- , pe.reference_no as 'Beleginfo - Inhalt 5'
- , 'Buchungstag' as 'Beleginfo - Art 6'
- , pe.reference_date as 'Beleginfo - Inhalt 6'
- , '' as 'Fälligkeit'
- """
-
- extra_joins = """
- LEFT JOIN `tabPayment Entry` pe
- ON gl.voucher_no = pe.name
- """
-
- extra_filters = """
- AND gl.voucher_type = 'Payment Entry'
- """
-
- return extra_fields, extra_joins, extra_filters
-
-
-def get_sales_invoice_params(filters):
- extra_fields = """
- , '' as 'Beleginfo - Art 5'
- , '' as 'Beleginfo - Inhalt 5'
- , '' as 'Beleginfo - Art 6'
- , '' as 'Beleginfo - Inhalt 6'
- , si.due_date as 'Fälligkeit'
- """
-
- extra_joins = """
- LEFT JOIN `tabSales Invoice` si
- ON gl.voucher_no = si.name
- """
-
- extra_filters = """
- AND gl.voucher_type = 'Sales Invoice'
- """
-
- return extra_fields, extra_joins, extra_filters
-
-
-def get_purchase_invoice_params(filters):
- extra_fields = """
- , 'Lieferanten-Rechnungsnummer' as 'Beleginfo - Art 5'
- , pi.bill_no as 'Beleginfo - Inhalt 5'
- , 'Lieferanten-Rechnungsdatum' as 'Beleginfo - Art 6'
- , pi.bill_date as 'Beleginfo - Inhalt 6'
- , pi.due_date as 'Fälligkeit'
- """
-
- extra_joins = """
- LEFT JOIN `tabPurchase Invoice` pi
- ON gl.voucher_no = pi.name
- """
-
- extra_filters = """
- AND gl.voucher_type = 'Purchase Invoice'
- """
-
- return extra_fields, extra_joins, extra_filters
-
-
-def get_generic_params(filters):
- # produce empty fields so all rows will have the same length
- extra_fields = """
- , '' as 'Beleginfo - Art 5'
- , '' as 'Beleginfo - Inhalt 5'
- , '' as 'Beleginfo - Art 6'
- , '' as 'Beleginfo - Inhalt 6'
- , '' as 'Fälligkeit'
- """
- extra_joins = ""
-
- if filters.get("exclude_voucher_types"):
- # exclude voucher types that are queried by a dedicated method
- exclude = "({})".format(
- ", ".join("'{}'".format(key) for key in filters.get("exclude_voucher_types"))
- )
- extra_filters = "AND gl.voucher_type NOT IN {}".format(exclude)
-
- # if voucher type filter is set, allow only this type
- if filters.get("voucher_type"):
- extra_filters += " AND gl.voucher_type = %(voucher_type)s"
-
- return extra_fields, extra_joins, extra_filters
-
-
-def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1):
- """
- 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]
- """
- query = """
- SELECT
-
- /* 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 */
- acc.account_number as 'Konto',
-
- /* against number or, if empty, party against number */
- %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
-
- '' as 'BU-Schlüssel',
-
- gl.posting_date as 'Belegdatum',
- gl.voucher_no as 'Belegfeld 1',
- REPLACE(LEFT(gl.remarks, 60), '\n', ' ') as 'Buchungstext',
- 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',
- gl.party_type as 'Beleginfo - Art 3',
- gl.party as 'Beleginfo - Inhalt 3',
- case gl.party_type when 'Customer' then 'Debitorennummer' when 'Supplier' then 'Kreditorennummer' else NULL end as 'Beleginfo - Art 4',
- par.debtor_creditor_number as 'Beleginfo - Inhalt 4'
-
- {extra_fields}
-
- FROM `tabGL Entry` gl
-
- /* Kontonummer */
- LEFT JOIN `tabAccount` acc
- ON gl.account = acc.name
-
- LEFT JOIN `tabParty Account` par
- ON par.parent = gl.party
- AND par.parenttype = gl.party_type
- AND par.company = %(company)s
-
- {extra_joins}
-
- WHERE gl.company = %(company)s
- AND DATE(gl.posting_date) >= %(from_date)s
- AND DATE(gl.posting_date) <= %(to_date)s
-
- {extra_filters}
-
- ORDER BY 'Belegdatum', gl.voucher_no""".format(
- extra_fields=extra_fields, extra_joins=extra_joins, extra_filters=extra_filters
- )
-
- gl_entries = frappe.db.sql(query, filters, as_dict=as_dict)
-
- return gl_entries
-
-
-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
-
- par.debtor_creditor_number as 'Konto',
- CASE cus.customer_type
- WHEN 'Company' THEN cus.customer_name
- ELSE null
- END as 'Name (Adressatentyp Unternehmen)',
- CASE cus.customer_type
- WHEN 'Individual' THEN TRIM(SUBSTR(cus.customer_name, LOCATE(' ', cus.customer_name)))
- ELSE null
- END as 'Name (Adressatentyp natürl. Person)',
- CASE cus.customer_type
- WHEN 'Individual' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(cus.customer_name, ' ', 1), ' ', -1)
- 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',
- adr.address_line1 as 'Straße',
- adr.pincode as 'Postleitzahl',
- adr.city as 'Ort',
- UPPER(country.code) as 'Land',
- adr.address_line2 as 'Adresszusatz',
- adr.email_id as 'E-Mail',
- adr.phone as 'Telefon',
- adr.fax as 'Fax',
- cus.website as 'Internet',
- cus.tax_id as 'Steuernummer'
-
- FROM `tabCustomer` cus
-
- left join `tabParty Account` par
- on par.parent = cus.name
- and par.parenttype = 'Customer'
- and par.company = %(company)s
-
- left join `tabDynamic Link` dyn_adr
- on dyn_adr.link_name = cus.name
- and dyn_adr.link_doctype = 'Customer'
- 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
-
- WHERE adr.is_primary_address = '1'
- """,
- filters,
- as_dict=1,
- )
-
-
-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
-
- par.debtor_creditor_number as 'Konto',
- CASE sup.supplier_type
- WHEN 'Company' THEN sup.supplier_name
- ELSE null
- END as 'Name (Adressatentyp Unternehmen)',
- CASE sup.supplier_type
- WHEN 'Individual' THEN TRIM(SUBSTR(sup.supplier_name, LOCATE(' ', sup.supplier_name)))
- ELSE null
- END as 'Name (Adressatentyp natürl. Person)',
- CASE sup.supplier_type
- WHEN 'Individual' THEN SUBSTRING_INDEX(SUBSTRING_INDEX(sup.supplier_name, ' ', 1), ' ', -1)
- 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',
- adr.address_line1 as 'Straße',
- adr.pincode as 'Postleitzahl',
- adr.city as 'Ort',
- UPPER(country.code) as 'Land',
- adr.address_line2 as 'Adresszusatz',
- adr.email_id as 'E-Mail',
- adr.phone as 'Telefon',
- adr.fax as 'Fax',
- 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 `tabSupplier` sup
-
- left join `tabParty Account` par
- on par.parent = sup.name
- and par.parenttype = 'Supplier'
- and par.company = %(company)s
-
- 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
-
- WHERE adr.is_primary_address = '1'
- """,
- filters,
- as_dict=1,
- )
-
-
-def get_account_names(filters):
- 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,
- )
-
-
-@frappe.whitelist()
-def download_datev_csv(filters):
- """
- 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, str):
- filters = json.loads(filters)
-
- validate(filters)
- company = filters.get("company")
-
- fiscal_year = get_fiscal_year(date=filters.get("from_date"), company=company)
- filters["fiscal_year_start"] = fiscal_year[1]
-
- # set chart of accounts used
- coa = frappe.get_value("Company", company, "chart_of_accounts")
- filters["skr"] = "04" if "SKR04" in coa else ("03" if "SKR03" in coa else "")
-
- datev_settings = frappe.get_doc("DATEV Settings", company)
- filters["account_number_length"] = datev_settings.account_number_length
- filters["temporary_against_account_number"] = datev_settings.temporary_against_account_number
-
- transactions = get_transactions(filters)
- account_names = get_account_names(filters)
- customers = get_customers(filters)
- suppliers = get_suppliers(filters)
-
- zip_name = "{} DATEV.zip".format(frappe.utils.datetime.date.today())
- zip_and_download(
- zip_name,
- [
- {
- "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
deleted file mode 100644
index 0df8c06..0000000
--- a/erpnext/regional/report/datev/test_datev.py
+++ /dev/null
@@ -1,252 +0,0 @@
-import zipfile
-from io import BytesIO
-from unittest import TestCase
-
-import frappe
-from frappe.utils import cstr, now_datetime, today
-
-from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-from erpnext.regional.germany.utils.datev.datev_constants import (
- AccountNames,
- DebtorsCreditors,
- Transactions,
-)
-from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, get_header
-from erpnext.regional.report.datev.datev import (
- download_datev_csv,
- get_account_names,
- get_customers,
- get_suppliers,
- get_transactions,
-)
-
-
-def make_company(company_name, abbr):
- if not frappe.db.exists("Company", company_name):
- company = frappe.get_doc(
- {
- "doctype": "Company",
- "company_name": company_name,
- "abbr": abbr,
- "default_currency": "EUR",
- "country": "Germany",
- "create_chart_of_accounts_based_on": "Standard Template",
- "chart_of_accounts": "SKR04 mit Kontonummern",
- }
- )
- company.insert()
- else:
- company = frappe.get_doc("Company", company_name)
-
- # indempotent
- company.create_default_warehouses()
-
- if not frappe.db.get_value("Cost Center", {"is_group": 0, "company": company.name}):
- company.create_default_cost_center()
-
- company.save()
- return company
-
-
-def setup_fiscal_year():
- fiscal_year = None
- year = cstr(now_datetime().year)
- if not frappe.db.get_value("Fiscal Year", {"year": year}, "name"):
- try:
- fiscal_year = frappe.get_doc(
- {
- "doctype": "Fiscal Year",
- "year": year,
- "year_start_date": "{0}-01-01".format(year),
- "year_end_date": "{0}-12-31".format(year),
- }
- )
- fiscal_year.insert()
- except frappe.NameError:
- pass
-
- if fiscal_year:
- fiscal_year.set_as_default()
-
-
-def make_customer_with_account(customer_name, company):
- acc_name = frappe.db.get_value(
- "Account", {"account_name": customer_name, "company": company.name}, "name"
- )
-
- if not acc_name:
- acc = frappe.get_doc(
- {
- "doctype": "Account",
- "parent_account": "1 - Forderungen aus Lieferungen und Leistungen - _TG",
- "account_name": customer_name,
- "company": company.name,
- "account_type": "Receivable",
- "account_number": "10001",
- }
- )
- acc.insert()
- acc_name = acc.name
-
- if not frappe.db.exists("Customer", customer_name):
- customer = frappe.get_doc(
- {
- "doctype": "Customer",
- "customer_name": customer_name,
- "customer_type": "Company",
- "accounts": [{"company": company.name, "account": acc_name}],
- }
- )
- customer.insert()
- else:
- customer = frappe.get_doc("Customer", customer_name)
-
- return customer
-
-
-def make_item(item_code, company):
- warehouse_name = frappe.db.get_value(
- "Warehouse", {"warehouse_name": "Stores", "company": company.name}, "name"
- )
-
- if not frappe.db.exists("Item", item_code):
- item = frappe.get_doc(
- {
- "doctype": "Item",
- "item_code": item_code,
- "item_name": item_code,
- "description": item_code,
- "item_group": "All Item Groups",
- "is_stock_item": 0,
- "is_purchase_item": 0,
- "is_customer_provided_item": 0,
- "item_defaults": [{"default_warehouse": warehouse_name, "company": company.name}],
- }
- )
- item.insert()
- else:
- item = frappe.get_doc("Item", item_code)
- return item
-
-
-def make_datev_settings(company):
- if not frappe.db.exists("DATEV Settings", company.name):
- frappe.get_doc(
- {
- "doctype": "DATEV Settings",
- "client": company.name,
- "client_number": "12345",
- "consultant_number": "67890",
- "temporary_against_account_number": "9999",
- }
- ).insert()
-
-
-class TestDatev(TestCase):
- def setUp(self):
- self.company = make_company("_Test GmbH", "_TG")
- self.customer = make_customer_with_account("_Test Kunde GmbH", self.company)
- self.filters = {
- "company": self.company.name,
- "from_date": today(),
- "to_date": today(),
- "temporary_against_account_number": "9999",
- }
-
- make_datev_settings(self.company)
- item = make_item("_Test Item", self.company)
- setup_fiscal_year()
-
- warehouse = frappe.db.get_value(
- "Item Default", {"parent": item.name, "company": self.company.name}, "default_warehouse"
- )
-
- income_account = frappe.db.get_value(
- "Account", {"account_number": "4200", "company": self.company.name}, "name"
- )
-
- tax_account = frappe.db.get_value(
- "Account", {"account_number": "3806", "company": self.company.name}, "name"
- )
-
- si = create_sales_invoice(
- company=self.company.name,
- customer=self.customer.name,
- currency=self.company.default_currency,
- debit_to=self.customer.accounts[0].account,
- income_account="4200 - Erlöse - _TG",
- expense_account="6990 - Herstellungskosten - _TG",
- cost_center=self.company.cost_center,
- warehouse=warehouse,
- item=item.name,
- do_not_save=1,
- )
-
- si.append(
- "taxes",
- {
- "charge_type": "On Net Total",
- "account_head": tax_account,
- "description": "Umsatzsteuer 19 %",
- "rate": 19,
- "cost_center": self.company.cost_center,
- },
- )
-
- si.cost_center = self.company.cost_center
-
- si.save()
- si.submit()
-
- def test_columns(self):
- def is_subset(get_data, allowed_keys):
- """
- Validate that the dict contains only allowed keys.
-
- Params:
- get_data -- Function that returns a list of dicts.
- allowed_keys -- List of allowed keys
- """
- data = get_data(self.filters)
- if data == []:
- # No data and, therefore, no columns is okay
- return True
- actual_set = set(data[0].keys())
- # allowed set must be interpreted as unicode to match the actual set
- allowed_set = set({frappe.as_unicode(key) for key in allowed_keys})
- return actual_set.issubset(allowed_set)
-
- self.assertTrue(is_subset(get_transactions, Transactions.COLUMNS))
- self.assertTrue(is_subset(get_customers, DebtorsCreditors.COLUMNS))
- self.assertTrue(is_subset(get_suppliers, DebtorsCreditors.COLUMNS))
- self.assertTrue(is_subset(get_account_names, AccountNames.COLUMNS))
-
- def test_header(self):
- self.assertTrue(Transactions.DATA_CATEGORY in get_header(self.filters, Transactions))
- self.assertTrue(AccountNames.DATA_CATEGORY in get_header(self.filters, AccountNames))
- self.assertTrue(DebtorsCreditors.DATA_CATEGORY in get_header(self.filters, DebtorsCreditors))
-
- def test_csv(self):
- test_data = [
- {
- "Umsatz (ohne Soll/Haben-Kz)": 100,
- "Soll/Haben-Kennzeichen": "H",
- "Kontonummer": "4200",
- "Gegenkonto (ohne BU-Schlüssel)": "10000",
- "Belegdatum": today(),
- "Buchungstext": "No remark",
- "Beleginfo - Art 1": "Sales Invoice",
- "Beleginfo - Inhalt 1": "SINV-0001",
- }
- ]
- get_datev_csv(data=test_data, filters=self.filters, csv_class=Transactions)
-
- def test_download(self):
- """Assert that the returned file is a ZIP file."""
- download_datev_csv(self.filters)
-
- # zipfile.is_zipfile() expects a file-like object
- zip_buffer = BytesIO()
- zip_buffer.write(frappe.response["filecontent"])
-
- self.assertTrue(zipfile.is_zipfile(zip_buffer))
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index dd185fc..0de5b2d 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -233,7 +233,8 @@
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
- ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}]
+ ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}],
+ ["default_provisional_account", {"root_type": ["in", ["Liability", "Asset"]]}]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index 23301a6..ae8b488 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -377,6 +377,17 @@
}
}
+ frm.set_query('default_provisional_account', 'item_defaults', (doc, cdt, cdn) => {
+ let row = locals[cdt][cdn];
+ return {
+ filters: {
+ "company": row.company,
+ "root_type": ["in", ["Liability", "Asset"]],
+ "is_group": 0
+ }
+ };
+ });
+
},
make_dashboard: function(frm) {
diff --git a/erpnext/stock/doctype/item_default/item_default.json b/erpnext/stock/doctype/item_default/item_default.json
index bc17160..042d398 100644
--- a/erpnext/stock/doctype/item_default/item_default.json
+++ b/erpnext/stock/doctype/item_default/item_default.json
@@ -15,6 +15,7 @@
"default_supplier",
"column_break_8",
"expense_account",
+ "default_provisional_account",
"selling_defaults",
"selling_cost_center",
"column_break_12",
@@ -101,11 +102,17 @@
"fieldtype": "Link",
"label": "Default Discount Account",
"options": "Account"
+ },
+ {
+ "fieldname": "default_provisional_account",
+ "fieldtype": "Link",
+ "label": "Default Provisional Account",
+ "options": "Account"
}
],
"istable": 1,
"links": [],
- "modified": "2021-07-13 01:26:03.860065",
+ "modified": "2022-04-10 20:18:54.148195",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Default",
@@ -114,5 +121,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 6e5f6f5..19c490d 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -106,8 +106,6 @@
"terms",
"bill_no",
"bill_date",
- "accounting_details_section",
- "provisional_expense_account",
"more_info",
"project",
"status",
@@ -1145,26 +1143,13 @@
"label": "Represents Company",
"options": "Company",
"read_only": 1
- },
- {
- "collapsible": 1,
- "fieldname": "accounting_details_section",
- "fieldtype": "Section Break",
- "label": "Accounting Details"
- },
- {
- "fieldname": "provisional_expense_account",
- "fieldtype": "Link",
- "hidden": 1,
- "label": "Provisional Expense Account",
- "options": "Account"
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2022-03-10 11:40:52.690984",
+ "modified": "2022-04-10 22:50:37.761362",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 1e1c0b9..ec0e809 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -145,10 +145,13 @@
)
)
- if provisional_accounting_for_non_stock_items:
- default_provisional_account = self.get_company_default("default_provisional_account")
- if not self.provisional_expense_account:
- self.provisional_expense_account = default_provisional_account
+ if not provisional_accounting_for_non_stock_items:
+ return
+
+ default_provisional_account = self.get_company_default("default_provisional_account")
+ for item in self.get("items"):
+ if not item.get("provisional_expense_account"):
+ item.provisional_expense_account = default_provisional_account
def validate_with_previous_doc(self):
super(PurchaseReceipt, self).validate_with_previous_doc(
@@ -509,7 +512,9 @@
and flt(d.qty)
and provisional_accounting_for_non_stock_items
):
- self.add_provisional_gl_entry(d, gl_entries, self.posting_date)
+ self.add_provisional_gl_entry(
+ d, gl_entries, self.posting_date, d.get("provisional_expense_account")
+ )
if warehouse_with_no_account:
frappe.msgprint(
@@ -518,9 +523,10 @@
+ "\n".join(warehouse_with_no_account)
)
- def add_provisional_gl_entry(self, item, gl_entries, posting_date, reverse=0):
- provisional_expense_account = self.get("provisional_expense_account")
- credit_currency = get_account_currency(provisional_expense_account)
+ def add_provisional_gl_entry(
+ self, item, gl_entries, posting_date, provisional_account, reverse=0
+ ):
+ credit_currency = get_account_currency(provisional_account)
debit_currency = get_account_currency(item.expense_account)
expense_account = item.expense_account
remarks = self.get("remarks") or _("Accounting Entry for Service")
@@ -534,7 +540,7 @@
self.add_gl_entry(
gl_entries=gl_entries,
- account=provisional_expense_account,
+ account=provisional_account,
cost_center=item.cost_center,
debit=0.0,
credit=multiplication_factor * item.amount,
@@ -554,7 +560,7 @@
debit=multiplication_factor * item.amount,
credit=0.0,
remarks=remarks,
- against_account=provisional_expense_account,
+ against_account=provisional_account,
account_currency=debit_currency,
project=item.project,
voucher_detail_no=item.name,
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index f3faba4..ce3bd56 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -747,14 +747,13 @@
update_purchase_receipt_status,
)
- pr = make_purchase_receipt()
+ item = make_item()
+
+ pr = make_purchase_receipt(item_code=item.name)
update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
- pr.reload()
- pr.cancel()
-
def test_pr_billing_status(self):
"""Flow:
1. PO -> PR1 -> PI
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 03a4201..1c65ac8 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -96,7 +96,6 @@
"include_exploded_items",
"batch_no",
"rejected_serial_no",
- "expense_account",
"item_tax_rate",
"item_weight_details",
"weight_per_unit",
@@ -107,6 +106,10 @@
"manufacturer",
"column_break_16",
"manufacturer_part_no",
+ "accounting_details_section",
+ "expense_account",
+ "column_break_102",
+ "provisional_expense_account",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -971,12 +974,27 @@
"label": "Product Bundle",
"options": "Product Bundle",
"read_only": 1
+ },
+ {
+ "fieldname": "provisional_expense_account",
+ "fieldtype": "Link",
+ "label": "Provisional Expense Account",
+ "options": "Account"
+ },
+ {
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details"
+ },
+ {
+ "fieldname": "column_break_102",
+ "fieldtype": "Column Break"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2022-02-01 11:32:27.980524",
+ "modified": "2022-04-11 13:07:32.061402",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index d3a230e..324ff4f 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -345,6 +345,7 @@
"expense_account": expense_account
or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults),
"discount_account": get_default_discount_account(args, item_defaults),
+ "provisional_expense_account": get_provisional_account(args, item_defaults),
"cost_center": get_default_cost_center(
args, item_defaults, item_group_defaults, brand_defaults
),
@@ -699,6 +700,10 @@
)
+def get_provisional_account(args, item):
+ return item.get("default_provisional_account") or args.default_provisional_account
+
+
def get_default_discount_account(args, item):
return item.get("default_discount_account") or args.discount_account
diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json
index 1d6c39e..a41ddb1 100644
--- a/erpnext/telephony/doctype/call_log/call_log.json
+++ b/erpnext/telephony/doctype/call_log/call_log.json
@@ -1,7 +1,7 @@
{
"actions": [],
"autoname": "field:id",
- "creation": "2019-06-05 12:07:02.634534",
+ "creation": "2022-02-21 11:54:58.414784",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
@@ -9,6 +9,8 @@
"id",
"from",
"to",
+ "call_received_by",
+ "employee_user_id",
"medium",
"start_time",
"end_time",
@@ -20,6 +22,7 @@
"recording_url",
"recording_html",
"section_break_11",
+ "type_of_call",
"summary",
"section_break_19",
"links"
@@ -103,7 +106,8 @@
},
{
"fieldname": "summary",
- "fieldtype": "Small Text"
+ "fieldtype": "Small Text",
+ "label": "Summary"
},
{
"fieldname": "section_break_11",
@@ -134,15 +138,37 @@
"fieldname": "call_details_section",
"fieldtype": "Section Break",
"label": "Call Details"
+ },
+ {
+ "fieldname": "employee_user_id",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Employee User Id",
+ "options": "User"
+ },
+ {
+ "fieldname": "type_of_call",
+ "fieldtype": "Link",
+ "label": "Type Of Call",
+ "options": "Telephony Call Type"
+ },
+ {
+ "depends_on": "to",
+ "fieldname": "call_received_by",
+ "fieldtype": "Link",
+ "label": "Call Received By",
+ "options": "Employee",
+ "read_only": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-02-08 14:23:28.744844",
+ "modified": "2022-04-14 02:59:22.503202",
"modified_by": "Administrator",
"module": "Telephony",
"name": "Call Log",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
@@ -164,6 +190,7 @@
],
"sort_field": "creation",
"sort_order": "DESC",
+ "states": [],
"title_field": "from",
"track_changes": 1,
"track_views": 1
diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 1c88883..7725e71 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -32,6 +32,10 @@
if lead:
self.add_link(link_type="Lead", link_name=lead)
+ # Add Employee Name
+ if self.is_incoming_call():
+ self.update_received_by()
+
def after_insert(self):
self.trigger_call_popup()
@@ -49,6 +53,9 @@
if not doc_before_save:
return
+ if self.is_incoming_call() and self.has_value_changed("to"):
+ self.update_received_by()
+
if _is_call_missed(doc_before_save, self):
frappe.publish_realtime("call_{id}_missed".format(id=self.id), self)
self.trigger_call_popup()
@@ -65,7 +72,8 @@
def trigger_call_popup(self):
if self.is_incoming_call():
scheduled_employees = get_scheduled_employees_for_popup(self.medium)
- employee_emails = get_employees_with_number(self.to)
+ employees = get_employees_with_number(self.to)
+ employee_emails = [employee.get("user_id") for employee in employees]
# check if employees with matched number are scheduled to receive popup
emails = set(scheduled_employees).intersection(employee_emails)
@@ -85,10 +93,17 @@
for email in emails:
frappe.publish_realtime("show_call_popup", self, user=email)
+ def update_received_by(self):
+ if employees := get_employees_with_number(self.get("to")):
+ self.call_received_by = employees[0].get("name")
+ self.employee_user_id = employees[0].get("user_id")
+
@frappe.whitelist()
-def add_call_summary(call_log, summary):
+def add_call_summary_and_call_type(call_log, summary, call_type):
doc = frappe.get_doc("Call Log", call_log)
+ doc.type_of_call = call_type
+ doc.save()
doc.add_comment("Comment", frappe.bold(_("Call Summary")) + "<br><br>" + summary)
@@ -97,20 +112,19 @@
if not number:
return []
- employee_emails = frappe.cache().hget("employees_with_number", number)
- if employee_emails:
- return employee_emails
+ employee_doc_name_and_emails = frappe.cache().hget("employees_with_number", number)
+ if employee_doc_name_and_emails:
+ return employee_doc_name_and_emails
- employees = frappe.get_all(
+ employee_doc_name_and_emails = frappe.get_all(
"Employee",
- filters={"cell_number": ["like", "%{}%".format(number)], "user_id": ["!=", ""]},
- fields=["user_id"],
+ filters={"cell_number": ["like", f"%{number}%"], "user_id": ["!=", ""]},
+ fields=["name", "user_id"],
)
- employee_emails = [employee.user_id for employee in employees]
- frappe.cache().hset("employees_with_number", number, employee_emails)
+ frappe.cache().hset("employees_with_number", number, employee_doc_name_and_emails)
- return employee_emails
+ return employee_doc_name_and_emails
def link_existing_conversations(doc, state):
diff --git a/erpnext/regional/doctype/datev_settings/__init__.py b/erpnext/telephony/doctype/telephony_call_type/__init__.py
similarity index 100%
rename from erpnext/regional/doctype/datev_settings/__init__.py
rename to erpnext/telephony/doctype/telephony_call_type/__init__.py
diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js
new file mode 100644
index 0000000..efba2b8
--- /dev/null
+++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Telephony Call Type', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json
new file mode 100644
index 0000000..603709e
--- /dev/null
+++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.json
@@ -0,0 +1,58 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "autoname": "field:call_type",
+ "creation": "2022-02-25 16:13:37.321312",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "call_type",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "call_type",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Call Type",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Telephony Call Type",
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2022-02-25 16:14:07.087461",
+ "modified_by": "Administrator",
+ "module": "Telephony",
+ "name": "Telephony Call Type",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py
new file mode 100644
index 0000000..944ffef
--- /dev/null
+++ b/erpnext/telephony/doctype/telephony_call_type/telephony_call_type.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class TelephonyCallType(Document):
+ pass
diff --git a/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py
new file mode 100644
index 0000000..b3c19c3
--- /dev/null
+++ b/erpnext/telephony/doctype/telephony_call_type/test_telephony_call_type.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestTelephonyCallType(unittest.TestCase):
+ pass
diff --git a/erpnext/tests/exotel_test_data.py b/erpnext/tests/exotel_test_data.py
new file mode 100644
index 0000000..3ad2575
--- /dev/null
+++ b/erpnext/tests/exotel_test_data.py
@@ -0,0 +1,122 @@
+import frappe
+
+call_initiation_data = frappe._dict(
+ {
+ "CallSid": "23c162077629863c1a2d7f29263a162m",
+ "CallFrom": "09999999991",
+ "CallTo": "09999999980",
+ "Direction": "incoming",
+ "Created": "Wed, 23 Feb 2022 12:31:59",
+ "From": "09999999991",
+ "To": "09999999988",
+ "CurrentTime": "2022-02-23 12:32:02",
+ "DialWhomNumber": "09999999999",
+ "Status": "busy",
+ "EventType": "Dial",
+ "AgentEmail": "test_employee_exotel@company.com",
+ }
+)
+
+call_end_data = frappe._dict(
+ {
+ "CallSid": "23c162077629863c1a2d7f29263a162m",
+ "CallFrom": "09999999991",
+ "CallTo": "09999999980",
+ "Direction": "incoming",
+ "ForwardedFrom": "null",
+ "Created": "Wed, 23 Feb 2022 12:31:59",
+ "DialCallDuration": "17",
+ "RecordingUrl": "https://s3-ap-southeast-1.amazonaws.com/random.mp3",
+ "StartTime": "2022-02-23 12:31:58",
+ "EndTime": "1970-01-01 05:30:00",
+ "DialCallStatus": "completed",
+ "CallType": "completed",
+ "DialWhomNumber": "09999999999",
+ "ProcessStatus": "null",
+ "flow_id": "228040",
+ "tenant_id": "67291",
+ "From": "09999999991",
+ "To": "09999999988",
+ "RecordingAvailableBy": "Wed, 23 Feb 2022 12:37:25",
+ "CurrentTime": "2022-02-23 12:32:25",
+ "OutgoingPhoneNumber": "09999999988",
+ "Legs": [
+ {
+ "Number": "09999999999",
+ "Type": "single",
+ "OnCallDuration": "10",
+ "CallerId": "09999999980",
+ "CauseCode": "NORMAL_CLEARING",
+ "Cause": "16",
+ }
+ ],
+ }
+)
+
+call_disconnected_data = frappe._dict(
+ {
+ "CallSid": "d96421addce69e24bdc7ce5880d1162l",
+ "CallFrom": "09999999991",
+ "CallTo": "09999999980",
+ "Direction": "incoming",
+ "ForwardedFrom": "null",
+ "Created": "Mon, 21 Feb 2022 15:58:12",
+ "DialCallDuration": "0",
+ "StartTime": "2022-02-21 15:58:12",
+ "EndTime": "1970-01-01 05:30:00",
+ "DialCallStatus": "canceled",
+ "CallType": "client-hangup",
+ "DialWhomNumber": "09999999999",
+ "ProcessStatus": "null",
+ "flow_id": "228040",
+ "tenant_id": "67291",
+ "From": "09999999991",
+ "To": "09999999988",
+ "CurrentTime": "2022-02-21 15:58:47",
+ "OutgoingPhoneNumber": "09999999988",
+ "Legs": [
+ {
+ "Number": "09999999999",
+ "Type": "single",
+ "OnCallDuration": "0",
+ "CallerId": "09999999980",
+ "CauseCode": "RING_TIMEOUT",
+ "Cause": "1003",
+ }
+ ],
+ }
+)
+
+call_not_answered_data = frappe._dict(
+ {
+ "CallSid": "fdb67a2b4b2d057b610a52ef43f81622",
+ "CallFrom": "09999999991",
+ "CallTo": "09999999980",
+ "Direction": "incoming",
+ "ForwardedFrom": "null",
+ "Created": "Mon, 21 Feb 2022 15:47:02",
+ "DialCallDuration": "0",
+ "StartTime": "2022-02-21 15:47:02",
+ "EndTime": "1970-01-01 05:30:00",
+ "DialCallStatus": "no-answer",
+ "CallType": "incomplete",
+ "DialWhomNumber": "09999999999",
+ "ProcessStatus": "null",
+ "flow_id": "228040",
+ "tenant_id": "67291",
+ "From": "09999999991",
+ "To": "09999999988",
+ "CurrentTime": "2022-02-21 15:47:40",
+ "OutgoingPhoneNumber": "09999999988",
+ "Legs": [
+ {
+ "Number": "09999999999",
+ "Type": "single",
+ "OnCallDuration": "0",
+ "CallerId": "09999999980",
+ "CauseCode": "RING_TIMEOUT",
+ "Cause": "1003",
+ }
+ ],
+ }
+)
diff --git a/erpnext/tests/test_exotel.py b/erpnext/tests/test_exotel.py
new file mode 100644
index 0000000..76bbb3e
--- /dev/null
+++ b/erpnext/tests/test_exotel.py
@@ -0,0 +1,69 @@
+import frappe
+from frappe.contacts.doctype.contact.test_contact import create_contact
+from frappe.tests.test_api import FrappeAPITestCase
+
+from erpnext.hr.doctype.employee.test_employee import make_employee
+
+
+class TestExotel(FrappeAPITestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.CURRENT_DB_CONNECTION = frappe.db
+ cls.test_employee_name = make_employee(
+ user="test_employee_exotel@company.com", cell_number="9999999999"
+ )
+ frappe.db.set_value("Exotel Settings", "Exotel Settings", "enabled", 1)
+ phones = [{"phone": "+91 9999999991", "is_primary_phone": 0, "is_primary_mobile_no": 1}]
+ create_contact(name="Test Contact", salutation="Mr", phones=phones)
+ frappe.db.commit()
+
+ def test_for_successful_call(self):
+ from .exotel_test_data import call_end_data, call_initiation_data
+
+ api_method = "handle_incoming_call"
+ end_call_api_method = "handle_end_call"
+
+ self.emulate_api_call_from_exotel(api_method, call_initiation_data)
+ self.emulate_api_call_from_exotel(end_call_api_method, call_end_data)
+ call_log = frappe.get_doc("Call Log", call_initiation_data.CallSid)
+
+ self.assertEqual(call_log.get("from"), call_initiation_data.CallFrom)
+ self.assertEqual(call_log.get("to"), call_initiation_data.DialWhomNumber)
+ self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
+ self.assertEqual(call_log.get("status"), "Completed")
+
+ def test_for_disconnected_call(self):
+ from .exotel_test_data import call_disconnected_data
+
+ api_method = "handle_missed_call"
+ self.emulate_api_call_from_exotel(api_method, call_disconnected_data)
+ call_log = frappe.get_doc("Call Log", call_disconnected_data.CallSid)
+ self.assertEqual(call_log.get("from"), call_disconnected_data.CallFrom)
+ self.assertEqual(call_log.get("to"), call_disconnected_data.DialWhomNumber)
+ self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
+ self.assertEqual(call_log.get("status"), "Canceled")
+
+ def test_for_call_not_answered(self):
+ from .exotel_test_data import call_not_answered_data
+
+ api_method = "handle_missed_call"
+ self.emulate_api_call_from_exotel(api_method, call_not_answered_data)
+ call_log = frappe.get_doc("Call Log", call_not_answered_data.CallSid)
+ self.assertEqual(call_log.get("from"), call_not_answered_data.CallFrom)
+ self.assertEqual(call_log.get("to"), call_not_answered_data.DialWhomNumber)
+ self.assertEqual(call_log.get("call_received_by"), self.test_employee_name)
+ self.assertEqual(call_log.get("status"), "No Answer")
+
+ def emulate_api_call_from_exotel(self, api_method, data):
+ self.post(
+ f"/api/method/erpnext.erpnext_integrations.exotel_integration.{api_method}",
+ data=frappe.as_json(data),
+ content_type="application/json",
+ as_tuple=True,
+ )
+ # restart db connection to get latest data
+ frappe.connect()
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db = cls.CURRENT_DB_CONNECTION
diff --git a/requirements.txt b/requirements.txt
index 657054f..85ff515 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,7 +1,6 @@
# frappe # https://github.com/frappe/frappe is installed during bench-init
gocardless-pro~=1.22.0
googlemaps
-pandas>=1.1.5,<2.0.0
plaid-python~=7.2.1
pycountry~=20.7.3
PyGithub~=1.55