diff --git a/README.md b/README.md index 80ebdb6b2a..0f6a52142b 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ ERPNext as a monolith includes the following areas for managing businesses: 1. [Accounting](https://erpnext.com/open-source-accounting) -1. [Inventory](https://erpnext.com/distribution/inventory-management-system) +1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system) 1. [CRM](https://erpnext.com/open-source-crm) 1. [Sales](https://erpnext.com/open-source-sales-purchase) 1. [Purchase](https://erpnext.com/open-source-sales-purchase) diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json index 856c371ecf..8915f79b92 100644 --- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json +++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json @@ -38,8 +38,8 @@ "reqd": 1 } ], - "modified": "2020-06-18 20:27:42.615842", - "modified_by": "ahmad@havenir.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Accounts", "name": "Item Tax Template", "owner": "Administrator", diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json index f3df1f0bc9..50fc3bbab7 100644 --- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json +++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.json @@ -45,11 +45,11 @@ ], "icon": "fa fa-credit-card", "idx": 1, - "modified": "2019-08-14 14:58:42.079115", - "modified_by": "sammish.thundiyil@gmail.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Accounts", "name": "Mode of Payment", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js index 9fc44bc1f0..e117471738 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.js +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js @@ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', { setup: function(frm) { frm.set_query("paid_from", function() { + frm.events.validate_company(frm); + var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; - return { filters: { "account_type": ["in", account_types], @@ -23,13 +24,16 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("party_type", function() { + frm.events.validate_company(frm); return{ filters: { "name": ["in", Object.keys(frappe.boot.party_account_types)], } } }); + frm.set_query("party_bank_account", function() { return { filters: { @@ -39,6 +43,7 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("bank_account", function() { return { filters: { @@ -47,6 +52,7 @@ frappe.ui.form.on('Payment Entry', { } } }); + frm.set_query("contact_person", function() { if (frm.doc.party) { return { @@ -58,10 +64,12 @@ frappe.ui.form.on('Payment Entry', { }; } }); + frm.set_query("paid_to", function() { + frm.events.validate_company(frm); + var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ? ["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; - return { filters: { "account_type": ["in", account_types], @@ -150,6 +158,12 @@ frappe.ui.form.on('Payment Entry', { frm.events.show_general_ledger(frm); }, + validate_company: (frm) => { + if (!frm.doc.company){ + frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}); + } + }, + company: function(frm) { frm.events.hide_unhide_fields(frm); frm.events.set_dynamic_labels(frm); diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json index 997937738b..72149a665d 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.json +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "autoname": "naming_series:", "creation": "2016-06-01 14:38:51.012597", @@ -63,6 +64,7 @@ "cost_center", "section_break_12", "status", + "custom_remarks", "remarks", "column_break_16", "letter_head", @@ -462,7 +464,8 @@ "fieldname": "remarks", "fieldtype": "Small Text", "label": "Remarks", - "no_copy": 1 + "no_copy": 1, + "read_only_depends_on": "eval:doc.custom_remarks == 0" }, { "fieldname": "column_break_16", @@ -573,10 +576,18 @@ "label": "Status", "options": "\nDraft\nSubmitted\nCancelled", "read_only": 1 + }, + { + "default": "0", + "fieldname": "custom_remarks", + "fieldtype": "Check", + "label": "Custom Remarks" } ], + "index_web_pages_for_search": 1, "is_submittable": 1, - "modified": "2019-12-08 13:02:30.016610", + "links": [], + "modified": "2020-09-02 13:39:43.383705", "modified_by": "Administrator", "module": "Accounts", "name": "Payment Entry", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bb312bf72e..11ab02021b 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -453,7 +453,7 @@ class PaymentEntry(AccountsController): frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) def set_remarks(self): - if self.remarks: return + if self.custom_remarks: return if self.payment_type=="Internal Transfer": remarks = [_("Amount {0} {1} transferred from {2} to {3}") diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json index d04f25b9ac..47546c07a4 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json @@ -291,11 +291,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 16:15:49.089450", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Accounts", "name": "Period Closing Voucher", - "owner": "jai@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 1, diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py index 15f23b63dc..b9e07b8030 100644 --- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py +++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py @@ -5,13 +5,14 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cint +from frappe.utils import cint, get_link_to_form from frappe.model.document import Document from erpnext.controllers.status_updater import StatusUpdater class POSOpeningEntry(StatusUpdater): def validate(self): self.validate_pos_profile_and_cashier() + self.validate_payment_method_account() self.set_status() def validate_pos_profile_and_cashier(self): @@ -20,6 +21,14 @@ class POSOpeningEntry(StatusUpdater): if not cint(frappe.db.get_value("User", self.user, "enabled")): frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user))) + + def validate_payment_method_account(self): + for d in self.balance_details: + account = frappe.db.get_value("Mode of Payment Account", + {"parent": d.mode_of_payment, "company": self.company}, "default_account") + if not account: + frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") + .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) def on_submit(self): self.set_status(update=True) \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.py b/erpnext/accounts/doctype/pos_profile/pos_profile.py index 789b4c3bd9..1386b70f55 100644 --- a/erpnext/accounts/doctype/pos_profile/pos_profile.py +++ b/erpnext/accounts/doctype/pos_profile/pos_profile.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import msgprint, _ -from frappe.utils import cint, now +from frappe.utils import cint, now, get_link_to_form from six import iteritems from frappe.model.document import Document @@ -13,7 +13,7 @@ class POSProfile(Document): self.validate_default_profile() self.validate_all_link_fields() self.validate_duplicate_groups() - self.check_default_payment() + self.validate_payment_methods() def validate_default_profile(self): for row in self.applicable_for_users: @@ -52,14 +52,23 @@ class POSProfile(Document): if len(customer_groups) != len(set(customer_groups)): frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group") - def check_default_payment(self): - if self.payments: - default_mode_of_payment = [d.default for d in self.payments if d.default] - if not default_mode_of_payment: - frappe.throw(_("Set default mode of payment")) + def validate_payment_methods(self): + if not self.payments: + frappe.throw(_("Payment methods are mandatory. Please add at least one payment method.")) - if len(default_mode_of_payment) > 1: - frappe.throw(_("Multiple default mode of payment is not allowed")) + default_mode_of_payment = [d.default for d in self.payments if d.default] + if not default_mode_of_payment: + frappe.throw(_("Please select a default mode of payment")) + + if len(default_mode_of_payment) > 1: + frappe.throw(_("You can only select one mode of payment as default")) + + for d in self.payments: + account = frappe.db.get_value("Mode of Payment Account", + {"parent": d.mode_of_payment, "company": self.company}, "default_account") + if not account: + frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") + .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) def on_update(self): self.set_defaults() diff --git a/erpnext/accounts/doctype/pos_settings/pos_settings.js b/erpnext/accounts/doctype/pos_settings/pos_settings.js index 504941d8b6..05cb7f0b4b 100644 --- a/erpnext/accounts/doctype/pos_settings/pos_settings.js +++ b/erpnext/accounts/doctype/pos_settings/pos_settings.js @@ -7,10 +7,10 @@ frappe.ui.form.on('POS Settings', { }, get_invoice_fields: function(frm) { - frappe.model.with_doctype("Sales Invoice", () => { - var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { + frappe.model.with_doctype("POS Invoice", () => { + var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || - d.fieldtype === 'Table') { + ['Table', 'Button'].includes(d.fieldtype)) { return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; } else { return null; @@ -25,7 +25,7 @@ frappe.ui.form.on('POS Settings', { frappe.ui.form.on("POS Field", { fieldname: function(frm, doctype, name) { var doc = frappe.get_doc(doctype, name); - var df = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { + var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { return doc.fieldname == d.fieldname ? d : null; })[0]; diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json index 0e748f84bb..f9fdc4b605 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json @@ -210,7 +210,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-12 14:53:47.679439", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges", diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json index a18fec61cf..b46d2e32f2 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json @@ -78,7 +78,7 @@ "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Taxes and Charges Template", - "owner": "wasim@webnotestech.com", + "owner": "Administrator", "permissions": [ { "email": 1, diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 71f2e120cc..92e49d59da 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe, erpnext import frappe.defaults -from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate +from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form from frappe import _, msgprint, throw from erpnext.accounts.party import get_party_account, get_due_date from frappe.model.mapper import get_mapped_doc @@ -1372,7 +1372,7 @@ def get_bank_cash_account(mode_of_payment, company): {"parent": mode_of_payment, "company": company}, "default_account") if not account: frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") - .format(mode_of_payment)) + .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) return { "account": account } @@ -1612,18 +1612,16 @@ def update_multi_mode_option(doc, pos_profile): payment.type = payment_mode.type doc.set('payments', []) - if not pos_profile or not pos_profile.get('payments'): - for payment_mode in get_all_mode_of_payments(doc): - append_payment(payment_mode) - return - for pos_payment_method in pos_profile.get('payments'): pos_payment_method = pos_payment_method.as_dict() payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) - if payment_mode: - payment_mode[0].default = pos_payment_method.default - append_payment(payment_mode[0]) + if not payment_mode: + frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") + .format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account")) + + payment_mode[0].default = pos_payment_method.default + append_payment(payment_mode[0]) def get_all_mode_of_payments(doc): return frappe.db.sql(""" diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 01d3903d28..c12e006d2b 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -320,7 +320,6 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, entry['remarks'] = "On cancellation of " + entry['voucher_no'] entry['is_cancelled'] = 1 - entry['posting_date'] = today() if entry['debit'] or entry['credit']: make_entry(entry, adv_adj, "Yes") diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js index efc76f9158..9703527875 100644 --- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js +++ b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js @@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation { check_plaid_status() { const me = this; frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { - if (r && r.enabled == "1") { + if (r && r.enabled === "1") { me.plaid_status = "active" } else { me.plaid_status = "inactive" @@ -139,7 +139,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { } make() { - const me = this; + const me = this; new frappe.ui.FileUploader({ method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement', allow_multiple: 0, @@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync { init_config() { const me = this; - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration') - .then(result => { - me.plaid_env = result.plaid_env; - me.plaid_public_key = result.plaid_public_key; - me.client_name = result.client_name; - me.sync_transactions() - }) + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration') + .then(result => { + me.plaid_env = result.plaid_env; + me.client_name = result.client_name; + me.link_token = result.link_token; + me.sync_transactions(); + }) } sync_transactions() { const me = this; - frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => { + frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => { frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', { - bank: v['bank'], + bank: r.bank, bank_account: me.parent.bank_account, freeze: true }) .then((result) => { - let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized") + let result_title = (result && result.length > 0) + ? __("{0} bank transaction(s) created", [result.length]) + : __("This bank account is already synchronized"); + let result_msg = ` -
-
${result_title}
-
` +
+
${result_title}
+
` + this.parent.$main_section.append(result_msg) - frappe.show_alert({message:__("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator:'green'}); + frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' }); }) }) } @@ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { }) frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments', - {bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")} + { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") } ).then((result) => { me.make_dialog(result) }) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index c999eb9b8e..29c4f7d394 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -34,7 +34,7 @@ frappe.query_reports["Accounts Receivable"] = { filters: { 'company': company } - } + }; } }, { diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 59117c8174..044fc1d3ab 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -617,9 +617,19 @@ class ReceivablePayableReport(object): elif party_type_field=="supplier": self.add_supplier_filters(conditions, values) + if self.filters.cost_center: + self.get_cost_center_conditions(conditions) + self.add_accounting_dimensions_filters(conditions, values) return " and ".join(conditions), values + def get_cost_center_conditions(self, conditions): + lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) + cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})] + + cost_center_string = '", "'.join(cost_center_list) + conditions.append('cost_center in ("{0}")'.format(cost_center_string)) + def get_order_by_condition(self): if self.filters.get('group_by_party'): return "order by party, posting_date" diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index 2563b66d1c..84c74543da 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -268,9 +268,9 @@ class GrossProfitGenerator(object): def get_last_purchase_rate(self, item_code, row): condition = '' if row.project: - condition += " AND a.project='%s'" % (row.project) + condition += " AND a.project=%s" % (frappe.db.escape(row.project)) elif row.cost_center: - condition += " AND a.cost_center='%s'" % (row.cost_center) + condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) if self.filters.to_date: condition += " AND modified='%s'" % (self.filters.to_date) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index fba20c0c87..7ad164a8b9 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -252,13 +252,6 @@ frappe.ui.form.on('Asset', { }) }, - available_for_use_date: function(frm) { - $.each(frm.doc.finance_books || [], function(i, d) { - if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date; - }); - refresh_field("finance_books"); - }, - is_existing_asset: function(frm) { frm.trigger("toggle_reference_doc"); // frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation)); @@ -438,6 +431,15 @@ frappe.ui.form.on('Asset Finance Book', { } frappe.flags.dont_change_rate = false; + }, + + depreciation_start_date: function(frm, cdt, cdn) { + const book = locals[cdt][cdn]; + if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) { + frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`)); + book.depreciation_start_date = ""; + frm.refresh_field("finance_books"); + } } }); diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9d08d9212d..72debb7eba 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -6,7 +6,7 @@ from __future__ import unicode_literals import frappe, erpnext, math, json from frappe import _ from six import string_types -from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days +from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day from frappe.model.document import Document from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset.depreciation \ @@ -83,6 +83,11 @@ class Asset(AccountsController): if not self.available_for_use_date: frappe.throw(_("Available for use date is required")) + for d in self.finance_books: + if d.depreciation_start_date == self.available_for_use_date: + frappe.throw(_("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format(d.idx), + title=_("Incorrect Date")) + def set_missing_values(self): if not self.asset_category: self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") @@ -294,7 +299,7 @@ class Asset(AccountsController): if not row.depreciation_start_date: if not self.available_for_use_date: frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) - row.depreciation_start_date = self.available_for_use_date + row.depreciation_start_date = get_last_day(self.available_for_use_date) if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index aed78e7746..52039c183b 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -371,19 +371,18 @@ class TestAsset(unittest.TestCase): asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') asset = frappe.get_doc('Asset', asset_name) asset.calculate_depreciation = 1 - asset.available_for_use_date = nowdate() - asset.purchase_date = nowdate() + asset.available_for_use_date = '2020-01-01' + asset.purchase_date = '2020-01-01' asset.append("finance_books", { "expected_value_after_useful_life": 10000, "depreciation_method": "Straight Line", - "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": nowdate() + "total_number_of_depreciations": 10, + "frequency_of_depreciation": 1 }) asset.insert() asset.submit() - post_depreciation_entries(date=add_months(nowdate(), 10)) + post_depreciation_entries(date=add_months('2020-01-01', 4)) scrap_asset(asset.name) @@ -392,9 +391,9 @@ class TestAsset(unittest.TestCase): self.assertTrue(asset.journal_entry_for_scrap) expected_gle = ( - ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), + ("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), ("_Test Fixed Asset - _TC", 0.0, 100000.0), - ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) + ("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0) ) gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` @@ -466,8 +465,7 @@ class TestAsset(unittest.TestCase): "expected_value_after_useful_life": 10000, "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) asset.insert() accumulated_depreciation_after_full_schedule = \ diff --git a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json index c80f95e155..79fcb957d4 100644 --- a/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json +++ b/erpnext/assets/doctype/asset_finance_book/asset_finance_book.json @@ -1,347 +1,99 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-05-08 14:44:37.095570", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "creation": "2018-05-08 14:44:37.095570", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "finance_book", + "depreciation_method", + "total_number_of_depreciations", + "column_break_5", + "frequency_of_depreciation", + "depreciation_start_date", + "expected_value_after_useful_life", + "value_after_depreciation", + "rate_of_depreciation" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "finance_book", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Finance Book", - "length": 0, - "no_copy": 0, - "options": "Finance Book", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "finance_book", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Finance Book", + "options": "Finance Book" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "depreciation_method", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Depreciation Method", - "length": 0, - "no_copy": 0, - "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "depreciation_method", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Depreciation Method", + "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "total_number_of_depreciations", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Total Number of Depreciations", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "total_number_of_depreciations", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Total Number of Depreciations", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "frequency_of_depreciation", - "fieldtype": "Int", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Frequency of Depreciation (Months)", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "frequency_of_depreciation", + "fieldtype": "Int", + "in_list_view": 1, + "label": "Frequency of Depreciation (Months)", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:parent.doctype == 'Asset'", - "fetch_if_empty": 0, - "fieldname": "depreciation_start_date", - "fieldtype": "Date", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Depreciation Start Date", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:parent.doctype == 'Asset'", + "fieldname": "depreciation_start_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Depreciation Posting Date", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0", - "depends_on": "eval:parent.doctype == 'Asset'", - "fetch_if_empty": 0, - "fieldname": "expected_value_after_useful_life", - "fieldtype": "Currency", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Expected Value After Useful Life", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "default": "0", + "depends_on": "eval:parent.doctype == 'Asset'", + "fieldname": "expected_value_after_useful_life", + "fieldtype": "Currency", + "label": "Expected Value After Useful Life", + "options": "Company:company:default_currency" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "value_after_depreciation", - "fieldtype": "Currency", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Value After Depreciation", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "value_after_depreciation", + "fieldtype": "Currency", + "hidden": 1, + "label": "Value After Depreciation", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.depreciation_method == 'Written Down Value'", - "description": "In Percentage", - "fetch_if_empty": 0, - "fieldname": "rate_of_depreciation", - "fieldtype": "Percent", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Rate of Depreciation", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "depends_on": "eval:doc.depreciation_method == 'Written Down Value'", + "description": "In Percentage", + "fieldname": "rate_of_depreciation", + "fieldtype": "Percent", + "label": "Rate of Depreciation" } - ], - "has_web_view": 0, - "hide_toolbar": 0, - "idx": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-04-09 19:45:14.523488", - "modified_by": "Administrator", - "module": "Assets", - "name": "Asset Finance Book", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "read_only": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2020-09-16 12:11:30.631788", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Finance Book", + "owner": "Administrator", + "permissions": [], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_movement/test_asset_movement.py b/erpnext/assets/doctype/asset_movement/test_asset_movement.py index c3755a3fb9..cddee5fa0f 100644 --- a/erpnext/assets/doctype/asset_movement/test_asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/test_asset_movement.py @@ -32,8 +32,7 @@ class TestAssetMovement(unittest.TestCase): "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) if asset.docstatus == 0: @@ -82,8 +81,7 @@ class TestAssetMovement(unittest.TestCase): "next_depreciation_date": "2020-12-31", "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 10, - "depreciation_start_date": "2020-06-06" + "frequency_of_depreciation": 10 }) if asset.docstatus == 0: asset.submit() diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 69817a4675..158799ce63 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -202,6 +202,53 @@ class TestPurchaseOrder(unittest.TestCase): self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) frappe.set_user("Administrator") + def test_update_child_with_tax_template(self): + tax_template = "_Test Account Excise Duty @ 10" + item = "_Test Item Home Desktop 100" + + if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): + item_doc = frappe.get_doc("Item", item) + item_doc.append("taxes", { + "item_tax_template": tax_template, + "valid_from": nowdate() + }) + item_doc.save() + else: + # update valid from + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}) + + po = create_purchase_order(item_code=item, qty=1, do_not_save=1) + + po.append("taxes", { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Purchase Taxes and Charges", + "rate": 10 + }) + po.insert() + po.submit() + + self.assertEqual(po.taxes[0].tax_amount, 50) + self.assertEqual(po.taxes[0].total, 550) + + items = json.dumps([ + {'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name}, + {'item_code' : item, 'rate' : 100, 'qty' : 1} # added item + ]) + update_child_qty_rate('Purchase Order', items, po.name) + + po.reload() + self.assertEqual(po.taxes[0].tax_amount, 60) + self.assertEqual(po.taxes[0].total, 660) + + frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL + where parent = %(item)s and item_tax_template = %(tax)s""", + {"item": item, "tax": tax_template}) + def test_update_child_uom_conv_factor_change(self): po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json index c3e1bf5303..d7ea9c1ccc 100644 --- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json +++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json @@ -148,11 +148,11 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-03-12 15:43:53.862897", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item Supplied", - "owner": "dhanalekshmi@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC" diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index ea5863020a..dc00bca5cc 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -188,11 +188,11 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-04-10 18:09:33.997618", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Receipt Item Supplied", - "owner": "wasim@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py index af109ba284..e956afdf74 100644 --- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py +++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py @@ -178,6 +178,7 @@ def make_all_scorecards(docname): period_card = make_supplier_scorecard(docname, None) period_card.start_date = start_date period_card.end_date = end_date + period_card.insert(ignore_permissions=True) period_card.submit() scp_count = scp_count + 1 if start_date < first_start_date: diff --git a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py index 87f10336f4..9938710e6e 100644 --- a/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py +++ b/erpnext/buying/doctype/supplier_scorecard_period/supplier_scorecard_period.py @@ -106,7 +106,7 @@ def make_supplier_scorecard(source_name, target_doc=None): "doctype": "Supplier Scorecard Scoring Criteria", "postprocess": update_criteria_fields, } - }, target_doc, post_process) + }, target_doc, post_process, ignore_permissions=True) return doc diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 9093cd570a..90c466b631 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -20,7 +20,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_t from erpnext.exceptions import InvalidCurrency from six import text_type from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions -from erpnext.stock.get_item_details import get_item_warehouse +from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map from erpnext.stock.doctype.packed_item.packed_item import make_packing_list force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules") @@ -1158,6 +1158,18 @@ def get_supplier_block_status(party_name): } return info +def set_child_tax_template_and_map(item, child_item, parent_doc): + args = { + 'item_code': item.item_code, + 'posting_date': parent_doc.transaction_date, + 'tax_category': parent_doc.get('tax_category'), + 'company': parent_doc.get('company') + } + + child_item.item_tax_template = _get_item_tax_template(args, item.taxes) + if child_item.get("item_tax_template"): + child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True) + def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item): """ Returns a Sales Order Item child item containing the default values @@ -1172,6 +1184,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, child_item.uom = trans_item.get("uom") or item.stock_uom conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor")) child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor + set_child_tax_template_and_map(item, child_item, p_doc) child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True) if not child_item.warehouse: frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.") @@ -1195,6 +1208,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor child_item.base_rate = 1 # Initiallize value will update in parent validation child_item.base_amount = 1 # Initiallize value will update in parent validation + set_child_tax_template_and_map(item, child_item, p_doc) return child_item def validate_and_delete_children(parent, data): @@ -1232,7 +1246,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil frappe.throw(_("You do not have permissions to {} items in a {}.") .format(actions[perm_type], parent_doctype), title=_("Insufficient Permissions")) - + def validate_workflow_conditions(doc): workflow = get_workflow_name(doc.doctype) if not workflow: @@ -1250,7 +1264,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil transitions.append(transition.as_dict()) if not transitions: - frappe.throw(_("You do not have workflow access to update this document."), title=_("Insufficient Workflow Permissions")) + frappe.throw( + _("You are not allowed to update as per the conditions set in {} Workflow.").format(get_link_to_form("Workflow", workflow)), + title=_("Insufficient Permissions") + ) def get_new_child_item(item_row): new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults @@ -1267,7 +1284,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil sales_doctypes = ['Sales Order', 'Sales Invoice', 'Delivery Note', 'Quotation'] parent = frappe.get_doc(parent_doctype, parent_doctype_name) - + check_doc_permissions(parent, 'cancel') validate_and_delete_children(parent, data) @@ -1315,7 +1332,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil child_item.conversion_factor = 1 else: child_item.conversion_factor = flt(d.get('conversion_factor')) - + if d.get("uom"): child_item.uom = d.get("uom") conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor")) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index c88bf66411..efd4944c34 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -165,9 +165,14 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters): AND company = %(company)s AND account_currency = %(currency)s AND `{searchfield}` LIKE %(txt)s + {mcond} ORDER BY idx DESC, name LIMIT %(offset)s, %(limit)s - """.format(account_type_condition=account_type_condition, searchfield=searchfield), + """.format( + account_type_condition=account_type_condition, + searchfield=searchfield, + mcond=get_match_cond(doctype) + ), dict( account_types=filters.get("account_type"), company=filters.get("company"), @@ -359,9 +364,21 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): if filters.get("is_return"): having_clause = "" + meta = frappe.get_meta("Batch", cached=True) + searchfields = meta.get_search_fields() + + search_columns = '' + if searchfields: + search_columns = ", " + ", ".join(searchfields) + if args.get('warehouse'): + searchfields = ['batch.' + field for field in searchfields] + if searchfields: + search_columns = ", " + ", ".join(searchfields) + batch_nos = frappe.db.sql("""select sle.batch_no, round(sum(sle.actual_qty),2), sle.stock_uom, concat('MFG-',batch.manufacturing_date), concat('EXP-',batch.expiry_date) + {search_columns} from `tabStock Ledger Entry` sle INNER JOIN `tabBatch` batch on sle.batch_no = batch.name where @@ -377,6 +394,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): group by batch_no {having_clause} order by batch.expiry_date, sle.batch_no desc limit %(start)s, %(page_len)s""".format( + search_columns = search_columns, cond=cond, match_conditions=get_match_cond(doctype), having_clause = having_clause @@ -384,7 +402,9 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): return batch_nos else: - return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) from `tabBatch` batch + return frappe.db.sql("""select name, concat('MFG-', manufacturing_date), concat('EXP-',expiry_date) + {search_columns} + from `tabBatch` batch where batch.disabled = 0 and item = %(item_code)s and (name like %(txt)s @@ -394,7 +414,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters): {0} {match_conditions} order by expiry_date, name desc - limit %(start)s, %(page_len)s""".format(cond, match_conditions=get_match_cond(doctype)), args) + limit %(start)s, %(page_len)s""".format(cond, search_columns = search_columns, match_conditions=get_match_cond(doctype)), args) @frappe.whitelist() diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index 17f3ae53e7..7f7aae31b1 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -81,6 +81,7 @@ class SellingController(StockController): party_details = _get_party_details(customer, ignore_permissions=self.flags.ignore_permissions, doctype=self.doctype, company=self.company, + posting_date=self.get('posting_date'), fetch_payment_terms_template=fetch_payment_terms_template, party_address=self.customer_address, shipping_address=self.shipping_address_name) if not self.meta.get_field("sales_team"): diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json index eb69dc06b6..d974beb2de 100644 --- a/erpnext/crm/desk_page/crm/crm.json +++ b/erpnext/crm/desk_page/crm/crm.json @@ -8,7 +8,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Opportunity\",\n \"name\": \"Minutes to First Response for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Opportunity\",\n \"name\": \"First Response Time for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]" }, { "hidden": 0, @@ -42,7 +42,7 @@ "idx": 0, "is_standard": 1, "label": "CRM", - "modified": "2020-05-28 13:33:52.906750", + "modified": "2020-08-11 18:55:18.238900", "modified_by": "Administrator", "module": "CRM", "name": "CRM", diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index b61cad3620..eee13f7e79 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -24,7 +24,7 @@ "converted_by", "sales_stage", "order_lost_reason", - "mins_to_first_response", + "first_response_time", "expected_closing", "next_contact", "contact_by", @@ -152,13 +152,6 @@ "no_copy": 1, "read_only": 1 }, - { - "bold": 1, - "fieldname": "mins_to_first_response", - "fieldtype": "Float", - "label": "Mins to first response", - "read_only": 1 - }, { "fieldname": "expected_closing", "fieldtype": "Date", @@ -419,12 +412,19 @@ "fieldtype": "Link", "label": "Converted By", "options": "User" + }, + { + "bold": 1, + "fieldname": "first_response_time", + "fieldtype": "Duration", + "label": "First Response Time", + "read_only": 1 } ], "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2020-08-11 17:34:35.066961", + "modified": "2020-08-12 17:34:35.066961", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/__init__.py b/erpnext/crm/report/first_response_time_for_opportunity/__init__.py similarity index 100% rename from erpnext/crm/report/minutes_to_first_response_for_opportunity/__init__.py rename to erpnext/crm/report/first_response_time_for_opportunity/__init__.py diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js similarity index 55% rename from erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js rename to erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js index 92d026a79c..3f5c95ab0a 100644 --- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.js +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.js @@ -1,33 +1,44 @@ // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors // For license information, please see license.txt +/* eslint-disable */ -frappe.query_reports["Minutes to First Response for Opportunity"] = { +frappe.query_reports["First Response Time for Opportunity"] = { "filters": [ { "fieldname": "from_date", "label": __("From Date"), "fieldtype": "Date", - 'reqd': 1, + "reqd": 1, "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) }, { "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", - 'reqd': 1, + "reqd": 1, "default": frappe.datetime.nowdate() }, ], - get_chart_data: function (columns, result) { + get_chart_data: function (_columns, result) { return { data: { labels: result.map(d => d[0]), datasets: [{ - name: 'Mins to first response', + name: "First Response Time", values: result.map(d => d[1]) }] }, - type: 'line', + type: "line", + tooltipOptions: { + formatTooltipY: d => { + let duration_options = { + hide_days: 0, + hide_seconds: 0 + }; + value = frappe.utils.get_formatted_duration(d, duration_options); + return value; + } + } } } -} +}; diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json new file mode 100644 index 0000000000..1b3184fe0a --- /dev/null +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.json @@ -0,0 +1,28 @@ +{ + "add_total_row": 0, + "creation": "2020-08-10 18:34:19.083872", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test 2", + "modified": "2020-08-10 18:34:19.083872", + "modified_by": "Administrator", + "module": "CRM", + "name": "First Response Time for Opportunity", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Opportunity", + "report_name": "First Response Time for Opportunity", + "report_type": "Script Report", + "roles": [ + { + "role": "Sales User" + }, + { + "role": "Sales Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py new file mode 100644 index 0000000000..2ffbc3e62a --- /dev/null +++ b/erpnext/crm/report/first_response_time_for_opportunity/first_response_time_for_opportunity.py @@ -0,0 +1,35 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe + +def execute(filters=None): + columns = [ + { + 'fieldname': 'creation_date', + 'label': 'Date', + 'fieldtype': 'Date', + 'width': 300 + }, + { + 'fieldname': 'first_response_time', + 'fieldtype': 'Duration', + 'label': 'First Response Time', + 'width': 300 + }, + ] + + data = frappe.db.sql(''' + SELECT + date(creation) as creation_date, + avg(first_response_time) as avg_response_time + FROM tabOpportunity + WHERE + date(creation) between %s and %s + and first_response_time > 0 + GROUP BY creation_date + ORDER BY creation_date desc + ''', (filters.from_date, filters.to_date)) + + return columns, data diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json b/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json deleted file mode 100644 index bcd092ba97..0000000000 --- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 0, - "creation": "2016-06-17 11:28:25.867258", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 20:06:08.801109", - "modified_by": "Administrator", - "module": "CRM", - "name": "Minutes to First Response for Opportunity", - "owner": "Administrator", - "ref_doctype": "Opportunity", - "report_name": "Minutes to First Response for Opportunity", - "report_type": "Script Report", - "roles": [ - { - "role": "Sales User" - }, - { - "role": "Sales Manager" - } - ] -} \ No newline at end of file diff --git a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py b/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py deleted file mode 100644 index 54e3a60308..0000000000 --- a/erpnext/crm/report/minutes_to_first_response_for_opportunity/minutes_to_first_response_for_opportunity.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe - -def execute(filters=None): - columns = [ - { - 'fieldname': 'creation_date', - 'label': 'Date', - 'fieldtype': 'Date' - }, - { - 'fieldname': 'mins', - 'fieldtype': 'Float', - 'label': 'Mins to First Response' - }, - ] - - data = frappe.db.sql('''select date(creation) as creation_date, - avg(mins_to_first_response) as mins - from tabOpportunity - where date(creation) between %s and %s - and mins_to_first_response > 0 - group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date)) - - return columns, data diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py index 532e19cffd..a033a2a722 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py @@ -2,81 +2,90 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -from frappe import _ -from frappe.utils.password import get_decrypted_password -from plaid import Client -from plaid.errors import APIError, ItemError +import plaid +import requests +from plaid.errors import APIError, ItemError, InvalidRequestError import frappe -import requests +from frappe import _ + class PlaidConnector(): def __init__(self, access_token=None): - - plaid_settings = frappe.get_single("Plaid Settings") - - self.config = { - "plaid_client_id": plaid_settings.plaid_client_id, - "plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'), - "plaid_public_key": plaid_settings.plaid_public_key, - "plaid_env": plaid_settings.plaid_env - } - - self.client = Client(client_id=self.config.get("plaid_client_id"), - secret=self.config.get("plaid_secret"), - public_key=self.config.get("plaid_public_key"), - environment=self.config.get("plaid_env") - ) - self.access_token = access_token + self.settings = frappe.get_single("Plaid Settings") + self.products = ["auth", "transactions"] + self.client_name = frappe.local.site + self.client = plaid.Client( + client_id=self.settings.plaid_client_id, + secret=self.settings.get_password("plaid_secret"), + environment=self.settings.plaid_env, + api_version="2019-05-29" + ) def get_access_token(self, public_token): if public_token is None: frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error")) - response = self.client.Item.public_token.exchange(public_token) - access_token = response['access_token'] - + access_token = response["access_token"] return access_token + def get_link_token(self): + token_request = { + "client_name": self.client_name, + "client_id": self.settings.plaid_client_id, + "secret": self.settings.plaid_secret, + "products": self.products, + # only allow Plaid-supported languages and countries (LAST: Sep-19-2020) + "language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en", + "country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"], + "user": { + "client_user_id": frappe.generate_hash(frappe.session.user, length=32) + } + } + + try: + response = self.client.LinkToken.create(token_request) + except InvalidRequestError: + frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error")) + frappe.msgprint(_("Please check your Plaid client ID and secret values")) + except APIError as e: + frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) + frappe.throw(_(str(e)), title=_("Authentication Failed")) + else: + return response["link_token"] + def auth(self): try: self.client.Auth.get(self.access_token) - print("Authentication successful.....") except ItemError as e: - if e.code == 'ITEM_LOGIN_REQUIRED': - pass - else: + if e.code == "ITEM_LOGIN_REQUIRED": pass except APIError as e: - if e.code == 'PLANNED_MAINTENANCE': - pass - else: + if e.code == "PLANNED_MAINTENANCE": pass except requests.Timeout: pass except Exception as e: - print(e) frappe.log_error(frappe.get_traceback(), _("Plaid authentication error")) - frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'}) + frappe.throw(_(str(e)), title=_("Authentication Failed")) def get_transactions(self, start_date, end_date, account_id=None): + self.auth() + kwargs = dict( + access_token=self.access_token, + start_date=start_date, + end_date=end_date + ) + if account_id: + kwargs.update(dict(account_ids=[account_id])) + try: - self.auth() - if account_id: - account_ids = [account_id] - - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids) - - else: - response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date) - - transactions = response['transactions'] - - while len(transactions) < response['total_transactions']: + response = self.client.Transactions.get(**kwargs) + transactions = response["transactions"] + while len(transactions) < response["total_transactions"]: response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions)) - transactions.extend(response['transactions']) + transactions.extend(response["transactions"]) return transactions except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js index 0ffbb877ea..22a4004955 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js @@ -4,14 +4,14 @@ frappe.provide("erpnext.integrations"); frappe.ui.form.on('Plaid Settings', { - enabled: function(frm) { + enabled: function (frm) { frm.toggle_reqd('plaid_client_id', frm.doc.enabled); frm.toggle_reqd('plaid_secret', frm.doc.enabled); - frm.toggle_reqd('plaid_public_key', frm.doc.enabled); frm.toggle_reqd('plaid_env', frm.doc.enabled); }, - refresh: function(frm) { - if(frm.doc.enabled) { + + refresh: function (frm) { + if (frm.doc.enabled) { frm.add_custom_button('Link a new bank account', () => { new erpnext.integrations.plaidLink(frm); }); @@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', { erpnext.integrations.plaidLink = class plaidLink { constructor(parent) { this.frm = parent; - this.product = ["transactions", "auth"]; this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js'; this.init_config(); } - init_config() { - const me = this; - me.plaid_env = me.frm.doc.plaid_env; - me.plaid_public_key = me.frm.doc.plaid_public_key; - me.client_name = frappe.boot.sitename; - me.init_plaid(); + async init_config() { + this.product = ["auth", "transactions"]; + this.plaid_env = this.frm.doc.plaid_env; + this.client_name = frappe.boot.sitename; + this.token = await this.frm.call("get_link_token").then(resp => resp.message); + this.init_plaid(); } init_plaid() { @@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink { } onScriptLoaded(me) { - me.linkHandler = window.Plaid.create({ + me.linkHandler = Plaid.create({ clientName: me.client_name, + product: me.product, env: me.plaid_env, - key: me.plaid_public_key, - onSuccess: me.plaid_success, - product: me.product + token: me.token, + onSuccess: me.plaid_success }); } onScriptError(error) { - frappe.msgprint('There was an issue loading the link-initialize.js script'); + frappe.msgprint("There was an issue connecting to Plaid's authentication server"); frappe.msgprint(error); } @@ -87,21 +86,25 @@ erpnext.integrations.plaidLink = class plaidLink { const me = this; frappe.prompt({ - fieldtype:"Link", + fieldtype: "Link", options: "Company", - label:__("Company"), - fieldname:"company", - reqd:1 + label: __("Company"), + fieldname: "company", + reqd: 1 }, (data) => { me.company = data.company; - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response}) - .then((result) => { - frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response, - bank: result, company: me.company}); - }) - .then(() => { - frappe.show_alert({message:__("Bank accounts added"), indicator:'green'}); + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', { + token: token, + response: response + }).then((result) => { + frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', { + response: response, + bank: result, + company: me.company }); + }).then(() => { + frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' }); + }); }, __("Select a company"), __("Continue")); } }; diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json index d8203d7390..2706217223 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json @@ -1,5 +1,4 @@ { - "actions": [], "creation": "2018-10-25 10:02:48.656165", "doctype": "DocType", "editable_grid": 1, @@ -12,7 +11,6 @@ "plaid_client_id", "plaid_secret", "column_break_7", - "plaid_public_key", "plaid_env" ], "fields": [ @@ -41,12 +39,6 @@ "in_list_view": 1, "label": "Plaid Secret" }, - { - "fieldname": "plaid_public_key", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Plaid Public Key" - }, { "fieldname": "plaid_env", "fieldtype": "Select", @@ -69,8 +61,7 @@ } ], "issingle": 1, - "links": [], - "modified": "2020-02-07 15:21:11.616231", + "modified": "2020-09-12 02:31:44.542385", "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Plaid Settings", diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py index c3371ed5df..e535e81bde 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py @@ -2,30 +2,36 @@ # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import json -from frappe import _ -from frappe.model.document import Document + +import frappe from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector -from frappe.utils import getdate, formatdate, today, add_months +from frappe import _ from frappe.desk.doctype.tag.tag import add_tag +from frappe.model.document import Document +from frappe.utils import add_months, formatdate, getdate, today + class PlaidSettings(Document): - pass + @staticmethod + def get_link_token(): + plaid = PlaidConnector() + return plaid.get_link_token() + @frappe.whitelist() -def plaid_configuration(): +def get_plaid_configuration(): if frappe.db.get_single_value("Plaid Settings", "enabled"): plaid_settings = frappe.get_single("Plaid Settings") return { - "plaid_public_key": plaid_settings.plaid_public_key, "plaid_env": plaid_settings.plaid_env, + "link_token": plaid_settings.get_link_token(), "client_name": frappe.local.site } - else: - return "disabled" + + return "disabled" + @frappe.whitelist() def add_institution(token, response): @@ -33,6 +39,7 @@ def add_institution(token, response): plaid = PlaidConnector() access_token = plaid.get_access_token(token) + bank = None if not frappe.db.exists("Bank", response["institution"]["name"]): try: @@ -44,7 +51,6 @@ def add_institution(token, response): bank.insert() except Exception: frappe.throw(frappe.get_traceback()) - else: bank = frappe.get_doc("Bank", response["institution"]["name"]) bank.plaid_access_token = access_token @@ -52,6 +58,7 @@ def add_institution(token, response): return bank + @frappe.whitelist() def add_bank_accounts(response, bank, company): try: @@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company): new_account.insert() result.append(new_account.name) - except frappe.UniqueValidationError: - frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name)) + frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"])) except Exception: frappe.throw(frappe.get_traceback()) @@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company): return result + def add_account_type(account_type): try: frappe.get_doc({ @@ -122,10 +129,11 @@ def add_account_subtype(account_subtype): except Exception: frappe.throw(frappe.get_traceback()) + @frappe.whitelist() def sync_transactions(bank, bank_account): - '''Sync transactions based on the last integration date as the start date, after the sync is completed - add the transaction date of the oldest transaction as the last integration date''' + """Sync transactions based on the last integration date as the start date, after sync is completed + add the transaction date of the oldest transaction as the last integration date.""" last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date") if last_transaction_date: start_date = formatdate(last_transaction_date, "YYYY-MM-dd") @@ -147,10 +155,10 @@ def sync_transactions(bank, bank_account): len(result), bank_account, start_date, end_date)) frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date) - except Exception: frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error")) + def get_transactions(bank, bank_account=None, start_date=None, end_date=None): access_token = None @@ -168,6 +176,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None): return transactions + def new_bank_transaction(transaction): result = [] @@ -182,8 +191,8 @@ def new_bank_transaction(transaction): status = "Pending" if transaction["pending"] == "True" else "Settled" + tags = [] try: - tags = [] tags += transaction["category"] tags += ["Plaid Cat. {}".format(transaction["category_id"])] except KeyError: @@ -216,6 +225,7 @@ def new_bank_transaction(transaction): return result + def automatic_synchronization(): settings = frappe.get_doc("Plaid Settings", "Plaid Settings") @@ -223,4 +233,8 @@ def automatic_synchronization(): plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"]) for plaid_account in plaid_accounts: - frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name) + frappe.enqueue( + "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", + bank=plaid_account.bank, + bank_account=plaid_account.name + ) diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py index 1a063d6b6f..3c906374c4 100644 --- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py +++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py @@ -1,14 +1,17 @@ # -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import unittest -import frappe -from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts import json -from frappe.utils.response import json_handler +import unittest + +import frappe from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account +from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import ( + add_account_subtype, add_account_type, add_bank_accounts, + new_bank_transaction, get_plaid_configuration) +from frappe.utils.response import json_handler + class TestPlaidSettings(unittest.TestCase): def setUp(self): @@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase): def test_plaid_disabled(self): frappe.db.set_value("Plaid Settings", None, "enabled", 0) - self.assertTrue(plaid_configuration() == "disabled") + self.assertTrue(get_plaid_configuration() == "disabled") def test_add_account_type(self): add_account_type("brokerage") @@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase): 'mask': '0000', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'name': 'Plaid Checking' - }], + }], 'institution': { 'institution_id': 'ins_6', 'name': 'Citi' @@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase): 'mask': '0000', 'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK', 'name': 'Plaid Checking' - }], + }], 'institution': { 'institution_id': 'ins_6', 'name': 'Citi' @@ -152,4 +155,4 @@ class TestPlaidSettings(unittest.TestCase): new_bank_transaction(transactions) - self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1) \ No newline at end of file + self.assertTrue(len(frappe.get_all("Bank Transaction")) == 1) diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json index 5339c99155..2e10751f96 100644 --- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json +++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json @@ -258,8 +258,8 @@ } ], "issingle": 1, - "modified": "2020-05-28 12:32:11.384757", - "modified_by": "umair@erpnext.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "ERPNext Integrations", "name": "Shopify Settings", "owner": "Administrator", diff --git a/erpnext/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json b/erpnext/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json index 9faa5b2f03..cf54e82199 100644 --- a/erpnext/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json +++ b/erpnext/healthcare/doctype/healthcare_schedule_time_slot/healthcare_schedule_time_slot.json @@ -117,12 +117,12 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-05-08 13:45:57.226530", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Healthcare", "name": "Healthcare Schedule Time Slot", "name_case": "", - "owner": "rmehta@gmail.com", + "owner": "Administrator", "permissions": [], "quick_entry": 1, "read_only": 0, diff --git a/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.json b/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.json index cff100cc70..a21825ea8e 100644 --- a/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.json +++ b/erpnext/healthcare/doctype/practitioner_schedule/practitioner_schedule.json @@ -44,11 +44,11 @@ } ], "links": [], - "modified": "2020-01-31 12:21:45.975488", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Healthcare", "name": "Practitioner Schedule", - "owner": "rmehta@gmail.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 463ad6c94b..f8b6be70ca 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -282,6 +282,11 @@ auto_cancel_exempted_doctypes= [ ] scheduler_events = { + "cron": { + "0/30 * * * *": [ + "erpnext.utilities.doctype.video.video.update_youtube_data", + ] + }, "all": [ "erpnext.projects.doctype.project.project.project_status_update_reminder", "erpnext.healthcare.doctype.patient_appointment.patient_appointment.send_appointment_reminder", diff --git a/erpnext/hr/doctype/appraisal/appraisal.json b/erpnext/hr/doctype/appraisal/appraisal.json index 4f6da975d5..91201d4b82 100644 --- a/erpnext/hr/doctype/appraisal/appraisal.json +++ b/erpnext/hr/doctype/appraisal/appraisal.json @@ -696,11 +696,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-01-30 11:28:08.401459", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Appraisal", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/hr/doctype/appraisal_goal/appraisal_goal.json b/erpnext/hr/doctype/appraisal_goal/appraisal_goal.json index f22969b7c1..b8ec5a8afb 100644 --- a/erpnext/hr/doctype/appraisal_goal/appraisal_goal.json +++ b/erpnext/hr/doctype/appraisal_goal/appraisal_goal.json @@ -207,11 +207,11 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-11 03:27:57.897071", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Appraisal Goal", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [], "quick_entry": 0, "read_only": 0, diff --git a/erpnext/hr/doctype/appraisal_template/appraisal_template.json b/erpnext/hr/doctype/appraisal_template/appraisal_template.json index ac6e400e09..7329c35dee 100644 --- a/erpnext/hr/doctype/appraisal_template/appraisal_template.json +++ b/erpnext/hr/doctype/appraisal_template/appraisal_template.json @@ -113,11 +113,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-12-13 12:37:56.937023", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Appraisal Template", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.json b/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.json index 34ea5d8225..2858419f33 100644 --- a/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.json +++ b/erpnext/hr/doctype/appraisal_template_goal/appraisal_template_goal.json @@ -78,11 +78,11 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-11 03:27:57.979215", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Appraisal Template Goal", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [], "quick_entry": 0, "read_only": 0, diff --git a/erpnext/hr/doctype/attendance/attendance.json b/erpnext/hr/doctype/attendance/attendance.json index a656a7ea5f..134098f252 100644 --- a/erpnext/hr/doctype/attendance/attendance.json +++ b/erpnext/hr/doctype/attendance/attendance.json @@ -205,11 +205,11 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-05-29 13:51:37.177231", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Attendance", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [ { "cancel": 1, diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.json b/erpnext/hr/doctype/expense_claim/expense_claim.json index fa28470af8..e3e6e80616 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.json +++ b/erpnext/hr/doctype/expense_claim/expense_claim.json @@ -371,12 +371,12 @@ "idx": 1, "is_submittable": 1, "links": [], - "modified": "2020-06-15 12:43:04.099803", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim", "name_case": "Title Case", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 1, diff --git a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json index 3cce50e090..70a48f93b7 100644 --- a/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json +++ b/erpnext/hr/doctype/expense_claim_detail/expense_claim_detail.json @@ -120,11 +120,11 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2020-05-11 18:54:35.601592", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Detail", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC" diff --git a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json index cf2cbb4308..02ab4cb6ac 100644 --- a/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json +++ b/erpnext/hr/doctype/expense_claim_type/expense_claim_type.json @@ -48,11 +48,11 @@ "icon": "fa fa-flag", "idx": 1, "links": [], - "modified": "2019-12-11 13:38:59.534034", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Expense Claim Type", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/hr/doctype/upload_attendance/upload_attendance.json b/erpnext/hr/doctype/upload_attendance/upload_attendance.json index 16b7a83173..a1451fde1d 100644 --- a/erpnext/hr/doctype/upload_attendance/upload_attendance.json +++ b/erpnext/hr/doctype/upload_attendance/upload_attendance.json @@ -229,11 +229,11 @@ "issingle": 1, "istable": 0, "max_attachments": 1, - "modified": "2017-11-14 12:51:34.980103", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "HR", "name": "Upload Attendance", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/hub_node/data_migration_mapping/company_to_hub_company/company_to_hub_company.json b/erpnext/hub_node/data_migration_mapping/company_to_hub_company/company_to_hub_company.json index ae0d492d72..b1e421dada 100644 --- a/erpnext/hub_node/data_migration_mapping/company_to_hub_company/company_to_hub_company.json +++ b/erpnext/hub_node/data_migration_mapping/company_to_hub_company/company_to_hub_company.json @@ -40,8 +40,8 @@ "mapping_name": "Company to Hub Company", "mapping_type": "Push", "migration_id_field": "hub_sync_id", - "modified": "2018-02-14 15:57:05.571142", - "modified_by": "achilles@erpnext.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "name": "Company to Hub Company", "owner": "Administrator", "page_length": 10, diff --git a/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/hub_message_to_lead.json b/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/hub_message_to_lead.json index d2af755d04..d11abeb4b3 100644 --- a/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/hub_message_to_lead.json +++ b/erpnext/hub_node/data_migration_mapping/hub_message_to_lead/hub_message_to_lead.json @@ -21,10 +21,10 @@ "mapping_name": "Hub Message to Lead", "mapping_type": "Pull", "migration_id_field": "hub_sync_id", - "modified": "2018-02-14 15:57:05.606597", - "modified_by": "achilles@erpnext.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "name": "Hub Message to Lead", - "owner": "frappetest@gmail.com", + "owner": "Administrator", "page_length": 10, "remote_objectname": "Hub Message", "remote_primary_key": "name" diff --git a/erpnext/hub_node/doctype/hub_user/hub_user.json b/erpnext/hub_node/doctype/hub_user/hub_user.json index a660022736..f51ffb4387 100644 --- a/erpnext/hub_node/doctype/hub_user/hub_user.json +++ b/erpnext/hub_node/doctype/hub_user/hub_user.json @@ -121,8 +121,8 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-09-01 13:56:07.816894", - "modified_by": "netchamp@rawcoderz.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Hub Node", "name": "Hub User", "name_case": "", diff --git a/erpnext/hub_node/doctype/hub_users/hub_users.json b/erpnext/hub_node/doctype/hub_users/hub_users.json index 2027e72fa0..d42f3fdf1b 100644 --- a/erpnext/hub_node/doctype/hub_users/hub_users.json +++ b/erpnext/hub_node/doctype/hub_users/hub_users.json @@ -54,12 +54,12 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2018-03-06 04:41:17.916243", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Hub Node", "name": "Hub Users", "name_case": "", - "owner": "test1@example.com", + "owner": "Administrator", "permissions": [], "quick_entry": 1, "read_only": 0, diff --git a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.json b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.json index 4b49f1978a..e784f68fcf 100644 --- a/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.json +++ b/erpnext/hub_node/doctype/marketplace_settings/marketplace_settings.json @@ -352,12 +352,12 @@ "issingle": 1, "istable": 0, "max_attachments": 0, - "modified": "2019-02-01 14:21:16.729848", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Hub Node", "name": "Marketplace Settings", "name_case": "", - "owner": "netchamp@rawcoderz.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json index 3788bc3700..606d22f52b 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json @@ -813,7 +813,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2018-08-21 14:44:33.670332", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Schedule", diff --git a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json index 11925681df..32bfa0e324 100644 --- a/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json +++ b/erpnext/maintenance/doctype/maintenance_visit/maintenance_visit.json @@ -1001,11 +1001,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2020-07-15 14:44:44.911402", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 1, diff --git a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json index 84dc72cd8a..467441d841 100644 --- a/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json +++ b/erpnext/maintenance/doctype/maintenance_visit_purpose/maintenance_visit_purpose.json @@ -125,11 +125,11 @@ ], "idx": 1, "istable": 1, - "modified": "2019-10-03 14:55:52.786805", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Maintenance", "name": "Maintenance Visit Purpose", - "owner": "ashwini@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json index 7e24dacbe1..96392658f1 100644 --- a/erpnext/non_profit/doctype/donor/donor.json +++ b/erpnext/non_profit/doctype/donor/donor.json @@ -1,336 +1,105 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "field:email", - "beta": 0, - "creation": "2017-09-19 16:20:27.510196", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "field:email", + "creation": "2017-09-19 16:20:27.510196", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "donor_name", + "column_break_5", + "donor_type", + "email", + "image", + "address_contacts", + "address_html", + "column_break_9", + "contact_html" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "donor_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Donor Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "donor_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Donor Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "donor_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Donor Type", - "length": 0, - "no_copy": 0, - "options": "Donor Type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "donor_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Donor Type", + "options": "Donor Type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email", + "reqd": 1, + "unique": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address and Contact", - "length": 0, - "no_copy": 0, - "options": "fa fa-map-marker", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "depends_on": "eval:!doc.__islocal;", + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address and Contact", + "options": "fa fa-map-marker" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "unique": 0 + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-01-22 15:53:35.059946", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Donor", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "links": [], + "modified": "2020-09-16 23:46:04.083274", + "modified_by": "Administrator", + "module": "Non Profit", + "name": "Donor", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Non Profit Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "donor_name", - "track_changes": 1, - "track_seen": 0 + ], + "quick_entry": 1, + "restrict_to_domain": "Non Profit", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "donor_name", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json index 77cdb94b3d..992ef16d64 100644 --- a/erpnext/non_profit/doctype/member/member.json +++ b/erpnext/non_profit/doctype/member/member.json @@ -111,6 +111,7 @@ "options": "Supplier" }, { + "depends_on": "eval:!doc.__islocal;", "fieldname": "address_contacts", "fieldtype": "Section Break", "label": "Address and Contact", @@ -177,7 +178,7 @@ ], "image_field": "image", "links": [], - "modified": "2020-08-06 10:06:01.153564", + "modified": "2020-09-16 23:44:13.596948", "modified_by": "Administrator", "module": "Non Profit", "name": "Member", diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json index 95bb3a5d84..7f218966a0 100644 --- a/erpnext/non_profit/doctype/membership/membership.json +++ b/erpnext/non_profit/doctype/membership/membership.json @@ -90,9 +90,9 @@ }, { "fieldname": "currency", - "fieldtype": "Select", + "fieldtype": "Link", "label": "Currency", - "options": "USD\nINR" + "options": "Currency" }, { "fieldname": "amount", @@ -126,7 +126,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2020-07-31 13:57:02.328995", + "modified": "2020-09-19 14:28:11.532696", "modified_by": "Administrator", "module": "Non Profit", "name": "Membership", @@ -161,4 +161,4 @@ "sort_field": "modified", "sort_order": "DESC", "track_changes": 1 -} \ No newline at end of file +} diff --git a/erpnext/non_profit/doctype/volunteer/volunteer.json b/erpnext/non_profit/doctype/volunteer/volunteer.json index 052b288089..08b7f87b2a 100644 --- a/erpnext/non_profit/doctype/volunteer/volunteer.json +++ b/erpnext/non_profit/doctype/volunteer/volunteer.json @@ -1,580 +1,148 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 1, - "autoname": "field:email", - "beta": 0, - "creation": "2017-09-19 16:16:45.676019", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "actions": [], + "allow_rename": 1, + "autoname": "field:email", + "creation": "2017-09-19 16:16:45.676019", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "volunteer_name", + "column_break_5", + "volunteer_type", + "email", + "image", + "address_contacts", + "address_html", + "column_break_9", + "contact_html", + "volunteer_availability_and_skills_details", + "availability", + "availability_timeslot", + "column_break_12", + "volunteer_skills", + "section_break_15", + "note" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_name", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Volunteer Name", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "volunteer_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Volunteer Name", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_5", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_5", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_type", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Volunteer Type", - "length": 0, - "no_copy": 0, - "options": "Volunteer Type", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "volunteer_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Volunteer Type", + "options": "Volunteer Type", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "email", - "fieldtype": "Data", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Email", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, + "fieldname": "email", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Email", + "reqd": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Image", - "length": 0, - "no_copy": 1, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1, + "print_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_contacts", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address and Contact", - "length": 0, - "no_copy": 0, - "options": "fa fa-map-marker", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "depends_on": "eval:!doc.__islocal;", + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address and Contact", + "options": "fa fa-map-marker" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Address HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_html", - "fieldtype": "HTML", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Contact HTML", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_availability_and_skills_details", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Availability and Skills", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "volunteer_availability_and_skills_details", + "fieldtype": "Section Break", + "label": "Availability and Skills" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "availability", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Availability", - "length": 0, - "no_copy": 0, - "options": "\nWeekly\nWeekdays\nWeekends", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "availability", + "fieldtype": "Select", + "label": "Availability", + "options": "\nWeekly\nWeekdays\nWeekends" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "availability_timeslot", - "fieldtype": "Select", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Availability Timeslot", - "length": 0, - "no_copy": 0, - "options": "\nMorning\nAfternoon\nEvening\nAnytime", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "availability_timeslot", + "fieldtype": "Select", + "label": "Availability Timeslot", + "options": "\nMorning\nAfternoon\nEvening\nAnytime" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_12", - "fieldtype": "Column Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "column_break_12", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "volunteer_skills", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Volunteer Skills", - "length": 0, - "no_copy": 0, - "options": "Volunteer Skill", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "volunteer_skills", + "fieldtype": "Table", + "label": "Volunteer Skills", + "options": "Volunteer Skill" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_15", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "section_break_15", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "note", - "fieldtype": "Long Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Note", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "fieldname": "note", + "fieldtype": "Long Text", + "label": "Note" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_field": "image", - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-11-04 03:36:25.776211", - "modified_by": "Administrator", - "module": "Non Profit", - "name": "Volunteer", - "name_case": "", - "owner": "Administrator", + ], + "image_field": "image", + "links": [], + "modified": "2020-09-16 23:45:15.595952", + "modified_by": "Administrator", + "module": "Non Profit", + "name": "Volunteer", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Non Profit Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Non Profit Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "restrict_to_domain": "Non Profit", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "volunteer_name", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "restrict_to_domain": "Non Profit", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "volunteer_name", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9ce570e6d0..6087ce29aa 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -690,6 +690,7 @@ erpnext.patches.v12_0.set_valid_till_date_in_supplier_quotation erpnext.patches.v13_0.update_old_loans erpnext.patches.v12_0.set_serial_no_status #2020-05-21 erpnext.patches.v12_0.update_price_list_currency_in_bom +execute:frappe.reload_doctype('Dashboard') execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25 @@ -725,4 +726,6 @@ erpnext.patches.v12_0.rename_lost_reason_detail erpnext.patches.v13_0.drop_razorpay_payload_column erpnext.patches.v13_0.update_start_end_date_for_old_shift_assignment erpnext.patches.v13_0.setting_custom_roles_for_some_regional_reports +erpnext.patches.v13_0.rename_issue_doctype_fields erpnext.patches.v13_0.change_default_pos_print_format +erpnext.patches.v13_0.set_youtube_video_id diff --git a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py index 8e60d33f85..d2bcb12070 100644 --- a/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py +++ b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py @@ -4,17 +4,16 @@ from __future__ import unicode_literals import frappe + def execute(): frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings") plaid_settings = frappe.get_single("Plaid Settings") if plaid_settings.enabled: - if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \ - and frappe.conf.plaid_public_key and frappe.conf.plaid_secret): + if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret): plaid_settings.enabled = 0 else: plaid_settings.update({ "plaid_client_id": frappe.conf.plaid_client_id, - "plaid_public_key": frappe.conf.plaid_public_key, "plaid_env": frappe.conf.plaid_env, "plaid_secret": frappe.conf.plaid_secret }) diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py new file mode 100644 index 0000000000..5bd6596579 --- /dev/null +++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py @@ -0,0 +1,65 @@ +# Copyright (c) 2020, Frappe and Contributors +# License: GNU General Public License v3. See license.txt + +from __future__ import unicode_literals +import frappe +from frappe.model.utils.rename_field import rename_field + +def execute(): + if frappe.db.exists('DocType', 'Issue'): + issues = frappe.db.get_all('Issue', fields=['name', 'response_by_variance', 'resolution_by_variance', 'mins_to_first_response'], + order_by='creation desc') + frappe.reload_doc('support', 'doctype', 'issue') + + # rename fields + rename_map = { + 'agreement_fulfilled': 'agreement_status', + 'mins_to_first_response': 'first_response_time' + } + for old, new in rename_map.items(): + rename_field('Issue', old, new) + + # change fieldtype to duration + count = 0 + for entry in issues: + response_by_variance = convert_to_seconds(entry.response_by_variance, 'Hours') + resolution_by_variance = convert_to_seconds(entry.resolution_by_variance, 'Hours') + mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') + frappe.db.set_value('Issue', entry.name, { + 'response_by_variance': response_by_variance, + 'resolution_by_variance': resolution_by_variance, + 'first_response_time': mins_to_first_response + }) + # commit after every 100 updates + count += 1 + if count%100 == 0: + frappe.db.commit() + + if frappe.db.exists('DocType', 'Opportunity'): + opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') + frappe.reload_doc('crm', 'doctype', 'opportunity') + rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') + + # change fieldtype to duration + count = 0 + for entry in opportunities: + mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes') + frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response) + # commit after every 100 updates + count += 1 + if count%100 == 0: + frappe.db.commit() + + # renamed reports from "Minutes to First Response for Issues" to "First Response Time for Issues". Same for Opportunity + for report in ['Minutes to First Response for Issues', 'Minutes to First Response for Opportunity']: + if frappe.db.exists('Report', report): + frappe.delete_doc('Report', report) + + +def convert_to_seconds(value, unit): + seconds = 0 + if unit == 'Hours': + seconds = value * 3600 + if unit == 'Minutes': + seconds = value * 60 + return seconds diff --git a/erpnext/patches/v13_0/set_youtube_video_id.py b/erpnext/patches/v13_0/set_youtube_video_id.py new file mode 100644 index 0000000000..c3b49eb4fe --- /dev/null +++ b/erpnext/patches/v13_0/set_youtube_video_id.py @@ -0,0 +1,10 @@ +from __future__ import unicode_literals +import frappe +from erpnext.utilities.doctype.video.video import get_id_from_url + +def execute(): + frappe.reload_doc("utilities", "doctype","video") + + for video in frappe.get_all("Video", fields=["name", "url", "youtube_video_id"]): + if video.url and not video.youtube_video_id: + frappe.db.set_value("Video", video.name, "youtube_video_id", get_id_from_url(video.url)) \ No newline at end of file diff --git a/erpnext/projects/doctype/dependent_task/dependent_task.json b/erpnext/projects/doctype/dependent_task/dependent_task.json index e00a2b287a..858a554bad 100644 --- a/erpnext/projects/doctype/dependent_task/dependent_task.json +++ b/erpnext/projects/doctype/dependent_task/dependent_task.json @@ -48,8 +48,8 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-10-26 17:37:25.638190", - "modified_by": "rohitw1991@gmail.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Projects", "name": "Dependent Task", "name_case": "", diff --git a/erpnext/public/js/conf.js b/erpnext/public/js/conf.js index 2af9140f9e..eb709e5e85 100644 --- a/erpnext/public/js/conf.js +++ b/erpnext/public/js/conf.js @@ -11,7 +11,9 @@ $.extend(frappe.breadcrumbs.preferred, { "Territory": "Selling", "Sales Person": "Selling", "Sales Partner": "Selling", - "Brand": "Stock" + "Brand": "Stock", + "Maintenance Schedule": "Support", + "Maintenance Visit": "Support" }); $.extend(frappe.breadcrumbs.module_map, { diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index f4eaad58da..6e97d811fc 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -120,7 +120,7 @@ frappe.ui.form.on('Salary Structure', { var get_payment_mode_account = function(frm, mode_of_payment, callback) { if(!frm.doc.company) { - frappe.throw(__("Please select the Company first")); + frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}); } if(!mode_of_payment) { diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 6d58fd2f3c..be30086d34 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -595,7 +595,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ $.each(actual_taxes_dict, function(key, value) { if (value) total_actual_tax += value; }); - + return flt(this.frm.doc.grand_total - total_actual_tax, precision("grand_total")); } }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 792235f7a3..33911793f6 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -417,7 +417,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges", me.frm.doc.name); - if (!this.frm.doc.taxes_and_charges && this.frm.doc.taxes) { + if (!this.frm.doc.taxes_and_charges && this.frm.doc.taxes && this.frm.doc.taxes.length > 0) { return; } diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index 065326744c..44e75aee36 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -224,6 +224,10 @@ erpnext.utils.set_taxes = function(frm, triggered_from_field) { party = frm.doc.party_name; } + if (!frm.doc.company) { + frappe.throw(__("Kindly select the company first")); + } + frappe.call({ method: "erpnext.accounts.party.set_taxes", args: { @@ -292,4 +296,4 @@ erpnext.utils.get_shipping_address = function(frm, callback){ } else { frappe.msgprint(__("Select company first")); } -} \ No newline at end of file +} diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/__init__.py b/erpnext/regional/germany/utils/__init__.py similarity index 100% rename from erpnext/support/report/minutes_to_first_response_for_issues/__init__.py rename to erpnext/regional/germany/utils/__init__.py diff --git a/erpnext/regional/germany/utils/datev/__init__.py b/erpnext/regional/germany/utils/datev/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/regional/report/datev/datev_constants.py b/erpnext/regional/germany/utils/datev/datev_constants.py similarity index 89% rename from erpnext/regional/report/datev/datev_constants.py rename to erpnext/regional/germany/utils/datev/datev_constants.py index e063703005..63f9a777bb 100644 --- a/erpnext/regional/report/datev/datev_constants.py +++ b/erpnext/regional/germany/utils/datev/datev_constants.py @@ -460,80 +460,8 @@ ACCOUNT_NAME_COLUMNS = [ "Sprach-ID" ] -QUERY_REPORT_COLUMNS = [ - { - "label": "Umsatz (ohne Soll/Haben-Kz)", - "fieldname": "Umsatz (ohne Soll/Haben-Kz)", - "fieldtype": "Currency", - "width": 100 - }, - { - "label": "Soll/Haben-Kennzeichen", - "fieldname": "Soll/Haben-Kennzeichen", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Konto", - "fieldname": "Konto", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Gegenkonto (ohne BU-Schlüssel)", - "fieldname": "Gegenkonto (ohne BU-Schlüssel)", - "fieldtype": "Data", - "width": 100 - }, - { - "label": "Belegdatum", - "fieldname": "Belegdatum", - "fieldtype": "Date", - "width": 100 - }, - { - "label": "Belegfeld 1", - "fieldname": "Belegfeld 1", - "fieldtype": "Data", - "width": 150 - }, - { - "label": "Buchungstext", - "fieldname": "Buchungstext", - "fieldtype": "Text", - "width": 300 - }, - { - "label": "Beleginfo - Art 1", - "fieldname": "Beleginfo - Art 1", - "fieldtype": "Link", - "options": "DocType", - "width": 100 - }, - { - "label": "Beleginfo - Inhalt 1", - "fieldname": "Beleginfo - Inhalt 1", - "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 1", - "width": 150 - }, - { - "label": "Beleginfo - Art 2", - "fieldname": "Beleginfo - Art 2", - "fieldtype": "Link", - "options": "DocType", - "width": 100 - }, - { - "label": "Beleginfo - Inhalt 2", - "fieldname": "Beleginfo - Inhalt 2", - "fieldtype": "Dynamic Link", - "options": "Beleginfo - Art 2", - "width": 150 - } -] - class DataCategory(): + """Field of the CSV Header.""" DEBTORS_CREDITORS = "16" @@ -542,6 +470,7 @@ class DataCategory(): POSTING_TEXT_CONSTANTS = "67" class FormatName(): + """Field of the CSV Header, corresponds to DataCategory.""" DEBTORS_CREDITORS = "Debitoren/Kreditoren" diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py new file mode 100644 index 0000000000..aae734f8e2 --- /dev/null +++ b/erpnext/regional/germany/utils/datev/datev_csv.py @@ -0,0 +1,174 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import datetime +import zipfile +from csv import QUOTE_NONNUMERIC +from six import BytesIO + +import six +import frappe +import pandas as pd +from frappe import _ +from .datev_constants import DataCategory + + +def get_datev_csv(data, filters, csv_class): + """ + Fill in missing columns and return a CSV in DATEV Format. + + For automatic processing, DATEV requires the first line of the CSV file to + hold meta data such as the length of account numbers oder the category of + the data. + + Arguments: + data -- array of dictionaries + filters -- dict + csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS + """ + empty_df = pd.DataFrame(columns=csv_class.COLUMNS) + data_df = pd.DataFrame.from_records(data) + result = empty_df.append(data_df, sort=True) + + if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: + result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) + + if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: + result['Sprach-ID'] = 'de-DE' + + data = result.to_csv( + # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 + sep=str(';'), + # European decimal seperator + decimal=',', + # Windows "ANSI" encoding + encoding='latin_1', + # format date as DDMM + date_format='%d%m', + # Windows line terminator + line_terminator='\r\n', + # Do not number rows + index=False, + # Use all columns defined above + columns=csv_class.COLUMNS, + # Quote most fields, even currency values with "," separator + quoting=QUOTE_NONNUMERIC + ) + + if not six.PY2: + data = data.encode('latin_1') + + header = get_header(filters, csv_class) + header = ';'.join(header).encode('latin_1') + + # 1st Row: Header with meta data + # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. + # 3rd - nth Row: Data (Nutzdaten) + return header + b'\r\n' + data + + +def get_header(filters, csv_class): + description = filters.get('voucher_type', csv_class.FORMAT_NAME) + company = filters.get('company') + datev_settings = frappe.get_doc('DATEV Settings', {'client': company}) + default_currency = frappe.get_value('Company', company, 'default_currency') + coa = frappe.get_value('Company', company, 'chart_of_accounts') + coa_short_code = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') + + header = [ + # DATEV format + # "DTVF" = created by DATEV software, + # "EXTF" = created by other software + '"EXTF"', + # version of the DATEV format + # 141 = 1.41, + # 510 = 5.10, + # 720 = 7.20 + '700', + csv_class.DATA_CATEGORY, + '"%s"' % csv_class.FORMAT_NAME, + # Format version (regarding format name) + csv_class.FORMAT_VERSION, + # Generated on + datetime.datetime.now().strftime('%Y%m%d%H%M%S') + '000', + # Imported on -- stays empty + '', + # Origin. Any two symbols, will be replaced by "SV" on import. + '"EN"', + # I = Exported by + '"%s"' % frappe.session.user, + # J = Imported by -- stays empty + '', + # K = Tax consultant number (Beraternummer) + datev_settings.get('consultant_number', '0000000'), + # L = Tax client number (Mandantennummer) + datev_settings.get('client_number', '00000'), + # M = Start of the fiscal year (Wirtschaftsjahresbeginn) + frappe.utils.formatdate(frappe.defaults.get_user_default('year_start_date'), 'yyyyMMdd'), + # N = Length of account numbers (Sachkontenlänge) + datev_settings.get('account_number_length', '4'), + # O = Transaction batch start date (YYYYMMDD) + frappe.utils.formatdate(filters.get('from_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # P = Transaction batch end date (YYYYMMDD) + frappe.utils.formatdate(filters.get('to_date'), 'yyyyMMdd') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # Q = Description (for example, "Sales Invoice") Max. 30 chars + '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # R = Diktatkürzel + '', + # S = Buchungstyp + # 1 = Transaction batch (Finanzbuchführung), + # 2 = Annual financial statement (Jahresabschluss) + '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # T = Rechnungslegungszweck + # 0 oder leer = vom Rechnungslegungszweck unabhängig + # 50 = Handelsrecht + # 30 = Steuerrecht + # 64 = IFRS + # 40 = Kalkulatorik + # 11 = Reserviert + # 12 = Reserviert + '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # U = Festschreibung + # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1" + '0', + # V = Default currency, for example, "EUR" + '"%s"' % default_currency if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', + # reserviert + '', + # Derivatskennzeichen + '', + # reserviert + '', + # reserviert + '', + # SKR + '"%s"' % coa_short_code, + # Branchen-Lösungs-ID + '', + # reserviert + '', + # reserviert + '', + # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) + '' + ] + return header + + +def download_csv_files_as_zip(csv_data_list): + """ + Put CSV files in a zip archive and send that to the client. + + Params: + csv_data_list -- list of dicts [{'file_name': 'EXTF_Buchunsstapel.zip', 'csv_data': get_datev_csv()}] + """ + zip_buffer = BytesIO() + + datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) + for csv_file in csv_data_list: + datev_zip.writestr(csv_file.get('file_name'), csv_file.get('csv_data')) + datev_zip.close() + + frappe.response['filecontent'] = zip_buffer.getvalue() + frappe.response['filename'] = 'DATEV.zip' + frappe.response['type'] = 'binary' diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py index 7fec94e740..dd818e6054 100644 --- a/erpnext/regional/report/datev/datev.py +++ b/erpnext/regional/report/datev/datev.py @@ -9,31 +9,91 @@ Provide a report and downloadable CSV according to the German DATEV format. """ from __future__ import unicode_literals -import datetime import json -import zipfile -import six import frappe -import pandas as pd - from frappe import _ -from csv import QUOTE_NONNUMERIC -from six import BytesIO from six import string_types -from .datev_constants import DataCategory -from .datev_constants import Transactions -from .datev_constants import DebtorsCreditors -from .datev_constants import AccountNames -from .datev_constants import QUERY_REPORT_COLUMNS +from erpnext.regional.germany.utils.datev.datev_csv import download_csv_files_as_zip, get_datev_csv +from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames + +COLUMNS = [ + { + "label": "Umsatz (ohne Soll/Haben-Kz)", + "fieldname": "Umsatz (ohne Soll/Haben-Kz)", + "fieldtype": "Currency", + "width": 100 + }, + { + "label": "Soll/Haben-Kennzeichen", + "fieldname": "Soll/Haben-Kennzeichen", + "fieldtype": "Data", + "width": 100 + }, + { + "label": "Konto", + "fieldname": "Konto", + "fieldtype": "Data", + "width": 100 + }, + { + "label": "Gegenkonto (ohne BU-Schlüssel)", + "fieldname": "Gegenkonto (ohne BU-Schlüssel)", + "fieldtype": "Data", + "width": 100 + }, + { + "label": "Belegdatum", + "fieldname": "Belegdatum", + "fieldtype": "Date", + "width": 100 + }, + { + "label": "Belegfeld 1", + "fieldname": "Belegfeld 1", + "fieldtype": "Data", + "width": 150 + }, + { + "label": "Buchungstext", + "fieldname": "Buchungstext", + "fieldtype": "Text", + "width": 300 + }, + { + "label": "Beleginfo - Art 1", + "fieldname": "Beleginfo - Art 1", + "fieldtype": "Link", + "options": "DocType", + "width": 100 + }, + { + "label": "Beleginfo - Inhalt 1", + "fieldname": "Beleginfo - Inhalt 1", + "fieldtype": "Dynamic Link", + "options": "Beleginfo - Art 1", + "width": 150 + }, + { + "label": "Beleginfo - Art 2", + "fieldname": "Beleginfo - Art 2", + "fieldtype": "Link", + "options": "DocType", + "width": 100 + }, + { + "label": "Beleginfo - Inhalt 2", + "fieldname": "Beleginfo - Inhalt 2", + "fieldtype": "Dynamic Link", + "options": "Beleginfo - Art 2", + "width": 150 + } +] def execute(filters=None): """Entry point for frappe.""" validate(filters) - result = get_transactions(filters, as_dict=0) - columns = QUERY_REPORT_COLUMNS - - return columns, result + return COLUMNS, get_transactions(filters, as_dict=0) def validate(filters): @@ -240,146 +300,8 @@ def get_account_names(filters): """, filters, as_dict=1) -def get_datev_csv(data, filters, csv_class): - """ - Fill in missing columns and return a CSV in DATEV Format. - - For automatic processing, DATEV requires the first line of the CSV file to - hold meta data such as the length of account numbers oder the category of - the data. - - Arguments: - data -- array of dictionaries - filters -- dict - csv_class -- defines DATA_CATEGORY, FORMAT_NAME and COLUMNS - """ - empty_df = pd.DataFrame(columns=csv_class.COLUMNS) - data_df = pd.DataFrame.from_records(data) - - result = empty_df.append(data_df, sort=True) - - if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS: - result['Belegdatum'] = pd.to_datetime(result['Belegdatum']) - - if csv_class.DATA_CATEGORY == DataCategory.ACCOUNT_NAMES: - result['Sprach-ID'] = 'de-DE' - - data = result.to_csv( - # Reason for str(';'): https://github.com/pandas-dev/pandas/issues/6035 - sep=str(';'), - # European decimal seperator - decimal=',', - # Windows "ANSI" encoding - encoding='latin_1', - # format date as DDMM - date_format='%d%m', - # Windows line terminator - line_terminator='\r\n', - # Do not number rows - index=False, - # Use all columns defined above - columns=csv_class.COLUMNS, - # Quote most fields, even currency values with "," separator - quoting=QUOTE_NONNUMERIC - ) - - if not six.PY2: - data = data.encode('latin_1') - - header = get_header(filters, csv_class) - header = ';'.join(header).encode('latin_1') - - # 1st Row: Header with meta data - # 2nd Row: Data heading (Überschrift der Nutzdaten), included in `data` here. - # 3rd - nth Row: Data (Nutzdaten) - return header + b'\r\n' + data - - -def get_header(filters, csv_class): - description = filters.get('voucher_type', csv_class.FORMAT_NAME) - - header = [ - # DATEV format - # "DTVF" = created by DATEV software, - # "EXTF" = created by other software - '"EXTF"', - # version of the DATEV format - # 141 = 1.41, - # 510 = 5.10, - # 720 = 7.20 - '700', - csv_class.DATA_CATEGORY, - '"%s"' % csv_class.FORMAT_NAME, - # Format version (regarding format name) - csv_class.FORMAT_VERSION, - # Generated on - datetime.datetime.now().strftime("%Y%m%d%H%M%S") + '000', - # Imported on -- stays empty - '', - # Origin. Any two symbols, will be replaced by "SV" on import. - '"EN"', - # I = Exported by - '"%s"' % frappe.session.user, - # J = Imported by -- stays empty - '', - # K = Tax consultant number (Beraternummer) - filters.get('consultant_number', '0000000'), - # L = Tax client number (Mandantennummer) - filters.get('client_number', '00000'), - # M = Start of the fiscal year (Wirtschaftsjahresbeginn) - frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"), - # N = Length of account numbers (Sachkontenlänge) - '%d' % filters.get('acc_len', 4), - # O = Transaction batch start date (YYYYMMDD) - frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd") if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # P = Transaction batch end date (YYYYMMDD) - frappe.utils.formatdate(filters.get('to_date'), "yyyyMMdd") if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # Q = Description (for example, "Sales Invoice") Max. 30 chars - '"{}"'.format(_(description)) if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # R = Diktatkürzel - '', - # S = Buchungstyp - # 1 = Transaction batch (Finanzbuchführung), - # 2 = Annual financial statement (Jahresabschluss) - '1' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # T = Rechnungslegungszweck - # 0 oder leer = vom Rechnungslegungszweck unabhängig - # 50 = Handelsrecht - # 30 = Steuerrecht - # 64 = IFRS - # 40 = Kalkulatorik - # 11 = Reserviert - # 12 = Reserviert - '0' if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # U = Festschreibung - # TODO: Filter by Accounting Period. In export for closed Accounting Period, this will be "1" - '0', - # V = Default currency, for example, "EUR" - '"%s"' % filters.get('default_currency', 'EUR') if csv_class.DATA_CATEGORY == DataCategory.TRANSACTIONS else '', - # reserviert - '', - # Derivatskennzeichen - '', - # reserviert - '', - # reserviert - '', - # SKR - '"%s"' % filters.get('skr', '04'), - # Branchen-Lösungs-ID - '', - # reserviert - '', - # reserviert - '', - # Anwendungsinformation (Verarbeitungskennzeichen der abgebenden Anwendung) - '' - ] - return header - - @frappe.whitelist() -def download_datev_csv(filters=None): +def download_datev_csv(filters): """ Provide accounting entries for download in DATEV format. @@ -400,38 +322,26 @@ def download_datev_csv(filters=None): coa = frappe.get_value('Company', filters.get('company'), 'chart_of_accounts') filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '') - # set account number length - account_numbers = frappe.get_list('Account', fields=['account_number'], filters={'is_group': 0, 'account_number': ('!=', '')}) - filters['acc_len'] = max([len(a.account_number) for a in account_numbers]) - - filters['consultant_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'consultant_number') - filters['client_number'] = frappe.get_value('DATEV Settings', filters.get('company'), 'client_number') - filters['default_currency'] = frappe.get_value('Company', filters.get('company'), 'default_currency') - - # This is where my zip will be written - zip_buffer = BytesIO() - # This is my zip file - datev_zip = zipfile.ZipFile(zip_buffer, mode='w', compression=zipfile.ZIP_DEFLATED) - transactions = get_transactions(filters) - transactions_csv = get_datev_csv(transactions, filters, csv_class=Transactions) - datev_zip.writestr('EXTF_Buchungsstapel.csv', transactions_csv) - account_names = get_account_names(filters) - account_names_csv = get_datev_csv(account_names, filters, csv_class=AccountNames) - datev_zip.writestr('EXTF_Kontenbeschriftungen.csv', account_names_csv) - customers = get_customers(filters) - customers_csv = get_datev_csv(customers, filters, csv_class=DebtorsCreditors) - datev_zip.writestr('EXTF_Kunden.csv', customers_csv) - suppliers = get_suppliers(filters) - suppliers_csv = get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors) - datev_zip.writestr('EXTF_Lieferanten.csv', suppliers_csv) - - # You must call close() before exiting your program or essential records will not be written. - datev_zip.close() - frappe.response['filecontent'] = zip_buffer.getvalue() - frappe.response['filename'] = 'DATEV.zip' - frappe.response['type'] = 'binary' + download_csv_files_as_zip([ + { + 'file_name': 'EXTF_Buchungsstapel.csv', + 'csv_data': get_datev_csv(transactions, filters, csv_class=Transactions) + }, + { + 'file_name': 'EXTF_Kontenbeschriftungen.csv', + 'csv_data': get_datev_csv(account_names, filters, csv_class=AccountNames) + }, + { + 'file_name': 'EXTF_Kunden.csv', + 'csv_data': get_datev_csv(customers, filters, csv_class=DebtorsCreditors) + }, + { + 'file_name': 'EXTF_Lieferanten.csv', + 'csv_data': get_datev_csv(suppliers, filters, csv_class=DebtorsCreditors) + }, + ]) diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py index eed62a8690..9529923a73 100644 --- a/erpnext/regional/report/datev/test_datev.py +++ b/erpnext/regional/report/datev/test_datev.py @@ -1,32 +1,22 @@ # coding=utf-8 from __future__ import unicode_literals -import os -import json import zipfile +import frappe from six import BytesIO from unittest import TestCase - -import frappe -from frappe.utils import getdate, today, now_datetime, cstr -from frappe.test_runner import make_test_objects +from frappe.utils import today, now_datetime, cstr from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice -from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import create_charts from erpnext.regional.report.datev.datev import validate from erpnext.regional.report.datev.datev import get_transactions from erpnext.regional.report.datev.datev import get_customers from erpnext.regional.report.datev.datev import get_suppliers from erpnext.regional.report.datev.datev import get_account_names -from erpnext.regional.report.datev.datev import get_datev_csv -from erpnext.regional.report.datev.datev import get_header from erpnext.regional.report.datev.datev import download_datev_csv -from erpnext.regional.report.datev.datev_constants import DataCategory -from erpnext.regional.report.datev.datev_constants import Transactions -from erpnext.regional.report.datev.datev_constants import DebtorsCreditors -from erpnext.regional.report.datev.datev_constants import AccountNames -from erpnext.regional.report.datev.datev_constants import QUERY_REPORT_COLUMNS +from erpnext.regional.germany.utils.datev.datev_csv import get_datev_csv, get_header +from erpnext.regional.germany.utils.datev.datev_constants import Transactions, DebtorsCreditors, AccountNames def make_company(company_name, abbr): if not frappe.db.exists("Company", company_name): diff --git a/erpnext/selling/doctype/industry_type/industry_type.json b/erpnext/selling/doctype/industry_type/industry_type.json index f4fcae428e..6c49f0f6dd 100644 --- a/erpnext/selling/doctype/industry_type/industry_type.json +++ b/erpnext/selling/doctype/industry_type/industry_type.json @@ -49,11 +49,11 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2016-07-25 05:24:25.881361", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Selling", "name": "Industry Type", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "amend": 0, diff --git a/erpnext/selling/doctype/product_bundle/product_bundle.json b/erpnext/selling/doctype/product_bundle/product_bundle.json index b63fb4bdcf..56155fb750 100644 --- a/erpnext/selling/doctype/product_bundle/product_bundle.json +++ b/erpnext/selling/doctype/product_bundle/product_bundle.json @@ -238,8 +238,8 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2017-10-18 14:23:06.538568", - "modified_by": "tundebabzy@gmail.com", + "modified": "2020-09-18 17:26:09.703215", + "modified_by": "Administrator", "module": "Selling", "name": "Product Bundle", "owner": "Administrator", diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index e1c54f64a7..7f0cabed8b 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -70,13 +70,23 @@ erpnext.PointOfSale.Payment = class { this.$invoice_fields.append( `
` ); + let df_events = { + onchange: function() { frm.set_value(this.df.fieldname, this.value); } + } + if (df.fieldtype == "Button") { + df_events = { + click: function() { + if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) { + frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname); + } + } + } + } this[`${df.fieldname}_field`] = frappe.ui.form.make_control({ df: { ...df, - onchange: function() { - frm.set_value(this.df.fieldname, this.value); - } + ...df_events }, parent: this.$invoice_fields.find(`.${df.fieldname}-field`), render_input: true, diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 0a70b97648..c716aa96e0 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -188,7 +188,7 @@ def get_conditions(filters): conditions += "AND so.transaction_date <= '%s'" %filters.to_date if filters.get("item_code"): - conditions += "AND so_item.item_code = '%s'" %frappe.db.escape(filters.item_code) + conditions += "AND so_item.item_code = %s" %frappe.db.escape(filters.item_code) if filters.get("customer"): conditions += "AND so.customer = %s" %frappe.db.escape(filters.customer) diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js index 80874c1deb..0e565a3fb6 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.js +++ b/erpnext/selling/report/sales_analytics/sales_analytics.js @@ -8,7 +8,7 @@ frappe.query_reports["Sales Analytics"] = { fieldname: "tree_type", label: __("Tree Type"), fieldtype: "Select", - options: ["Customer Group","Customer","Item Group","Item","Territory","Order Type"], + options: ["Customer Group", "Customer", "Item Group", "Item", "Territory", "Order Type", "Project"], default: "Customer", reqd: 1 }, diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.py b/erpnext/selling/report/sales_analytics/sales_analytics.py index 4d113c8e9e..d036a1cb09 100644 --- a/erpnext/selling/report/sales_analytics/sales_analytics.py +++ b/erpnext/selling/report/sales_analytics/sales_analytics.py @@ -34,7 +34,7 @@ class Analytics(object): def get_columns(self): self.columns = [{ - "label": _(self.filters.tree_type + " ID"), + "label": _(self.filters.tree_type), "options": self.filters.tree_type if self.filters.tree_type != "Order Type" else "", "fieldname": "entity", "fieldtype": "Link" if self.filters.tree_type != "Order Type" else "Data", @@ -97,6 +97,10 @@ class Analytics(object): self.get_sales_transactions_based_on_order_type() self.get_rows_by_group() + elif self.filters.tree_type == "Project": + self.get_sales_transactions_based_on_project() + self.get_rows() + def get_sales_transactions_based_on_order_type(self): if self.filters["value_quantity"] == 'Value': value_field = "base_net_total" @@ -198,6 +202,24 @@ class Analytics(object): self.get_groups() + def get_sales_transactions_based_on_project(self): + if self.filters["value_quantity"] == 'Value': + value_field = "base_net_total as value_field" + else: + value_field = "total_qty as value_field" + + entity = "project as entity" + + self.entries = frappe.get_all(self.filters.doc_type, + fields=[entity, value_field, self.date_field], + filters={ + "docstatus": 1, + "company": self.filters.company, + "project": ["!=", ""], + self.date_field: ('between', [self.filters.from_date, self.filters.to_date]) + } + ) + def get_rows(self): self.data = [] self.get_periodic_data() @@ -205,7 +227,7 @@ class Analytics(object): for entity, period_data in iteritems(self.entity_periodic_data): row = { "entity": entity, - "entity_name": self.entity_names.get(entity) + "entity_name": self.entity_names.get(entity) if hasattr(self, 'entity_names') else None } total = 0 for end_date in self.periodic_daterange: diff --git a/erpnext/stock/doctype/batch/batch.json b/erpnext/stock/doctype/batch/batch.json index 1eb457734e..943cb3401f 100644 --- a/erpnext/stock/doctype/batch/batch.json +++ b/erpnext/stock/doctype/batch/batch.json @@ -166,11 +166,11 @@ "idx": 1, "image_field": "image", "max_attachments": 5, - "modified": "2019-10-03 22:38:45.104056", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Stock", "name": "Batch", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 38e5fe53a7..faeeb578fe 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -26,19 +26,19 @@ frappe.ui.form.on("Item", { refresh: function(frm) { if (frm.doc.is_stock_item) { - frm.add_custom_button(__("Balance"), function() { + frm.add_custom_button(__("Stock Balance"), function() { frappe.route_options = { "item_code": frm.doc.name } frappe.set_route("query-report", "Stock Balance"); }, __("View")); - frm.add_custom_button(__("Ledger"), function() { + frm.add_custom_button(__("Stock Ledger"), function() { frappe.route_options = { "item_code": frm.doc.name } frappe.set_route("query-report", "Stock Ledger"); }, __("View")); - frm.add_custom_button(__("Projected"), function() { + frm.add_custom_button(__("Stock Projected Qty"), function() { frappe.route_options = { "item_code": frm.doc.name } diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json index 90a392c145..b24d621c31 100644 --- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json +++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json @@ -133,11 +133,11 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-12 15:41:21.053462", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Item", - "owner": "wasim@webnotestech.com", + "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC" diff --git a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json index f49c147682..9b2b5da9cb 100644 --- a/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json +++ b/erpnext/stock/doctype/landed_cost_purchase_receipt/landed_cost_purchase_receipt.json @@ -173,11 +173,11 @@ "issingle": 0, "istable": 1, "max_attachments": 0, - "modified": "2016-07-20 10:49:34.228751", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Purchase Receipt", - "owner": "wasim@webnotestech.com", + "owner": "Administrator", "permissions": [], "quick_entry": 0, "read_only": 0, diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py index 67161aa6dd..1e7153e774 100644 --- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py @@ -494,8 +494,7 @@ class TestPurchaseReceipt(unittest.TestCase): "expected_value_after_useful_life": 10, "depreciation_method": "Straight Line", "total_number_of_depreciations": 3, - "frequency_of_depreciation": 1, - "depreciation_start_date": frappe.utils.nowdate() + "frequency_of_depreciation": 1 }) asset.submit() diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 9845bc2f70..1f95447951 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -23,6 +23,24 @@ frappe.ui.form.on('Stock Entry', { } }); + frm.set_query('source_warehouse_address', function() { + return { + filters: { + link_doctype: 'Warehouse', + link_name: frm.doc.from_warehouse + } + } + }); + + frm.set_query('target_warehouse_address', function() { + return { + filters: { + link_doctype: 'Warehouse', + link_name: frm.doc.to_warehouse + } + } + }); + frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => { if (r.sample_retention_warehouse) { var filters = [ diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js index 23700c94ee..2499c801d2 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.js @@ -9,13 +9,15 @@ frappe.query_reports["Batch-Wise Balance History"] = { "fieldtype": "Date", "width": "80", "default": frappe.sys_defaults.year_start_date, + "reqd": 1 }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", "width": "80", - "default": frappe.datetime.get_today() + "default": frappe.datetime.get_today(), + "reqd": 1 }, { "fieldname": "item", diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 2c95084b81..8f3e246e7f 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -11,6 +11,9 @@ from frappe.utils import cint, flt, getdate def execute(filters=None): if not filters: filters = {} + if filters.from_date > filters.to_date: + frappe.throw(_("From Date must be before To Date")) + float_precision = cint(frappe.db.get_default("float_precision")) or 3 columns = get_columns(filters) diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json index b1ad7c8aa0..28410f3a71 100644 --- a/erpnext/support/desk_page/support/support.json +++ b/erpnext/support/desk_page/support/support.json @@ -28,7 +28,7 @@ { "hidden": 0, "label": "Reports", - "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"Minutes to First Response for Issues\",\n \"name\": \"Minutes to First Response for Issues\",\n \"type\": \"report\"\n }\n]" + "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]" } ], "category": "Modules", @@ -43,7 +43,7 @@ "idx": 0, "is_standard": 1, "label": "Support", - "modified": "2020-06-04 11:54:56.124219", + "modified": "2020-08-11 15:49:34.307341", "modified_by": "Administrator", "module": "Support", "name": "Support", diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 858564a527..fe01d4b983 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -2,10 +2,14 @@ frappe.ui.form.on("Issue", { onload: function(frm) { frm.email_field = "raised_by"; - frappe.db.get_value("Support Settings", {name: "Support Settings"}, "allow_resetting_service_level_agreement", (r) => { - if (!r.allow_resetting_service_level_agreement) { - frm.set_df_property("reset_service_level_agreement", "hidden", 1) ; - } + frappe.db.get_value("Support Settings", {name: "Support Settings"}, + ["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => { + if (r && r.track_service_level_agreement == "0") { + frm.set_df_property("service_level_section", "hidden", 1); + } + if (r && r.allow_resetting_service_level_agreement == "0") { + frm.set_df_property("reset_service_level_agreement", "hidden", 1); + } }); if (frm.doc.service_level_agreement) { @@ -38,7 +42,7 @@ frappe.ui.form.on("Issue", { }, refresh: function (frm) { - if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") { + if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") { if (frm.doc.service_level_agreement) { frappe.call({ 'method': 'frappe.client.get', @@ -85,14 +89,14 @@ frappe.ui.form.on("Issue", { if (frm.doc.service_level_agreement) { frm.dashboard.clear_headline(); - let agreement_fulfilled = (frm.doc.agreement_fulfilled == "Fulfilled") ? + let agreement_status = (frm.doc.agreement_status == "Fulfilled") ? {"indicator": "green", "msg": "Service Level Agreement has been fulfilled"} : {"indicator": "red", "msg": "Service Level Agreement Failed"}; frm.dashboard.set_headline_alert( '
' + '
' + - ' ' + + ' ' + '
' + '
' ); @@ -198,13 +202,13 @@ function set_time_to_resolve_and_response(frm) { frm.dashboard.clear_headline(); var time_to_respond = get_status(frm.doc.response_by_variance); - if (!frm.doc.first_responded_on && frm.doc.agreement_fulfilled === "Ongoing") { - time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_fulfilled); + if (!frm.doc.first_responded_on && frm.doc.agreement_status === "Ongoing") { + time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status); } var time_to_resolve = get_status(frm.doc.resolution_by_variance); - if (!frm.doc.resolution_date && frm.doc.agreement_fulfilled === "Ongoing") { - time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_fulfilled); + if (!frm.doc.resolution_date && frm.doc.agreement_status === "Ongoing") { + time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status); } frm.dashboard.set_headline_alert( @@ -219,10 +223,10 @@ function set_time_to_resolve_and_response(frm) { ); } -function get_time_left(timestamp, agreement_fulfilled) { +function get_time_left(timestamp, agreement_status) { const diff = moment(timestamp).diff(moment()); const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed"; - let indicator = (diff_display == 'Failed' && agreement_fulfilled != "Fulfilled") ? "red" : "green"; + let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green"; return {"diff_display": diff_display, "indicator": indicator}; } diff --git a/erpnext/support/doctype/issue/issue.json b/erpnext/support/doctype/issue/issue.json index 6525ab27d3..a43381c5c6 100644 --- a/erpnext/support/doctype/issue/issue.json +++ b/erpnext/support/doctype/issue/issue.json @@ -27,17 +27,25 @@ "response_by_variance", "reset_service_level_agreement", "cb", - "agreement_fulfilled", + "agreement_status", "resolution_by", "resolution_by_variance", "service_level_agreement_creation", "on_hold_since", "total_hold_time", "response", - "mins_to_first_response", + "first_response_time", "first_responded_on", "column_break_26", "avg_response_time", + "section_break_19", + "resolution_details", + "column_break1", + "opening_date", + "opening_time", + "resolution_date", + "resolution_time", + "user_resolution_time", "additional_info", "lead", "contact", @@ -46,23 +54,14 @@ "customer_name", "project", "company", - "section_break_19", - "resolution_details", - "column_break1", - "opening_date", - "opening_time", - "resolution_date", - "content_type", - "attachment", "via_customer_portal", - "resolution_time", - "user_resolution_time" + "attachment", + "content_type" ], "fields": [ { "fieldname": "subject_section", "fieldtype": "Section Break", - "label": "Subject", "options": "fa fa-flag" }, { @@ -158,7 +157,7 @@ "collapsible": 1, "fieldname": "service_level_section", "fieldtype": "Section Break", - "label": "Service Level" + "label": "Service Level Agreement Details" }, { "fieldname": "service_level_agreement", @@ -191,14 +190,7 @@ "collapsible": 1, "fieldname": "response", "fieldtype": "Section Break", - "label": "Response" - }, - { - "bold": 1, - "fieldname": "mins_to_first_response", - "fieldtype": "Float", - "label": "Mins to First Response", - "read_only": 1 + "label": "Response Details" }, { "fieldname": "first_responded_on", @@ -261,7 +253,7 @@ "collapsible": 1, "fieldname": "section_break_19", "fieldtype": "Section Break", - "label": "Resolution" + "label": "Resolution Details" }, { "depends_on": "eval:!doc.__islocal", @@ -326,28 +318,19 @@ "fieldtype": "Check", "label": "Via Customer Portal" }, - { - "default": "Ongoing", - "depends_on": "eval: doc.service_level_agreement", - "fieldname": "agreement_fulfilled", - "fieldtype": "Select", - "label": "Service Level Agreement Fulfilled", - "options": "Ongoing\nFulfilled\nFailed", - "read_only": 1 - }, { "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "description": "in hours", "fieldname": "response_by_variance", - "fieldtype": "Float", + "fieldtype": "Duration", + "hide_seconds": 1, "label": "Response By Variance", "read_only": 1 }, { "depends_on": "eval: doc.service_level_agreement && doc.status != 'Replied';", - "description": "in hours", "fieldname": "resolution_by_variance", - "fieldtype": "Float", + "fieldtype": "Duration", + "hide_seconds": 1, "label": "Resolution By Variance", "read_only": 1 }, @@ -406,12 +389,28 @@ "fieldtype": "Duration", "label": "Total Hold Time", "read_only": 1 + }, + { + "default": "Ongoing", + "depends_on": "eval: doc.service_level_agreement", + "fieldname": "agreement_status", + "fieldtype": "Select", + "label": "Service Level Agreement Status", + "options": "Ongoing\nFulfilled\nFailed", + "read_only": 1 + }, + { + "bold": 1, + "fieldname": "first_response_time", + "fieldtype": "Duration", + "label": "First Response Time", + "read_only": 1 } ], "icon": "fa fa-ticket", "idx": 7, "links": [], - "modified": "2020-06-10 12:47:37.146914", + "modified": "2020-08-11 18:49:07.574769", "modified_by": "Administrator", "module": "Support", "name": "Issue", diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py index 87168e151e..920c13c38d 100644 --- a/erpnext/support/doctype/issue/issue.py +++ b/erpnext/support/doctype/issue/issue.py @@ -61,7 +61,7 @@ class Issue(Document): if self.status in ["Closed", "Resolved"] and status not in ["Resolved", "Closed"]: self.resolution_date = frappe.flags.current_time or now_datetime() - if frappe.db.get_value("Issue", self.name, "agreement_fulfilled") == "Ongoing": + if frappe.db.get_value("Issue", self.name, "agreement_status") == "Ongoing": set_service_level_agreement_variance(issue=self.name) self.update_agreement_status() set_resolution_time(issue=self) @@ -72,7 +72,7 @@ class Issue(Document): self.resolution_date = None self.reset_issue_metrics() # enable SLA and variance on Reopen - self.agreement_fulfilled = "Ongoing" + self.agreement_status = "Ongoing" set_service_level_agreement_variance(issue=self.name) self.handle_hold_time(status) @@ -113,39 +113,39 @@ class Issue(Document): if not self.first_responded_on: response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) response_by = add_to_date(response_by, seconds=round(last_hold_time)) - response_by_variance = round(time_diff_in_hours(response_by, now_time)) + response_by_variance = round(time_diff_in_seconds(response_by, now_time)) update_values['response_by'] = response_by - update_values['response_by_variance'] = response_by_variance + (last_hold_time // 3600) + update_values['response_by_variance'] = response_by_variance + last_hold_time resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) resolution_by = add_to_date(resolution_by, seconds=round(last_hold_time)) - resolution_by_variance = round(time_diff_in_hours(resolution_by, now_time)) + resolution_by_variance = round(time_diff_in_seconds(resolution_by, now_time)) update_values['resolution_by'] = resolution_by - update_values['resolution_by_variance'] = resolution_by_variance + (last_hold_time // 3600) + update_values['resolution_by_variance'] = resolution_by_variance + last_hold_time update_values['on_hold_since'] = None self.db_set(update_values) def update_agreement_status(self): - if self.service_level_agreement and self.agreement_fulfilled == "Ongoing": + if self.service_level_agreement and self.agreement_status == "Ongoing": if frappe.db.get_value("Issue", self.name, "response_by_variance") < 0 or \ frappe.db.get_value("Issue", self.name, "resolution_by_variance") < 0: - self.agreement_fulfilled = "Failed" + self.agreement_status = "Failed" else: - self.agreement_fulfilled = "Fulfilled" + self.agreement_status = "Fulfilled" - def update_agreement_fulfilled_on_custom_status(self): + def update_agreement_status_on_custom_status(self): """ Update Agreement Fulfilled status using Custom Scripts for Custom Issue Status """ if not self.first_responded_on: # first_responded_on set when first reply is sent to customer - self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime()), 2) + self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime()), 2) if not self.resolution_date: # resolution_date set when issue has been closed - self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime()), 2) + self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime()), 2) - self.agreement_fulfilled = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" + self.agreement_status = "Fulfilled" if self.response_by_variance > 0 and self.resolution_by_variance > 0 else "Failed" def create_communication(self): communication = frappe.new_doc("Communication") @@ -172,7 +172,7 @@ class Issue(Document): replicated_issue = deepcopy(self) replicated_issue.subject = subject replicated_issue.issue_split_from = self.name - replicated_issue.mins_to_first_response = 0 + replicated_issue.first_response_time = 0 replicated_issue.first_responded_on = None replicated_issue.creation = now_datetime() @@ -180,7 +180,7 @@ class Issue(Document): if replicated_issue.service_level_agreement: replicated_issue.service_level_agreement_creation = now_datetime() replicated_issue.service_level_agreement = None - replicated_issue.agreement_fulfilled = "Ongoing" + replicated_issue.agreement_status = "Ongoing" replicated_issue.response_by = None replicated_issue.response_by_variance = None replicated_issue.resolution_by = None @@ -241,8 +241,8 @@ class Issue(Document): self.response_by = get_expected_time_for(parameter="response", service_level=priority, start_date_time=start_date_time) self.resolution_by = get_expected_time_for(parameter="resolution", service_level=priority, start_date_time=start_date_time) - self.response_by_variance = round(time_diff_in_hours(self.response_by, now_datetime())) - self.resolution_by_variance = round(time_diff_in_hours(self.resolution_by, now_datetime())) + self.response_by_variance = round(time_diff_in_seconds(self.response_by, now_datetime())) + self.resolution_by_variance = round(time_diff_in_seconds(self.resolution_by, now_datetime())) def change_service_level_agreement_and_priority(self): if self.service_level_agreement and frappe.db.exists("Issue", self.name) and \ @@ -271,7 +271,7 @@ class Issue(Document): self.service_level_agreement_creation = now_datetime() self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement) - self.agreement_fulfilled = "Ongoing" + self.agreement_status = "Ongoing" self.save() def reset_issue_metrics(self): @@ -347,7 +347,7 @@ def get_expected_time_for(parameter, service_level, start_date_time): def set_service_level_agreement_variance(issue=None): current_time = frappe.flags.current_time or now_datetime() - filters = {"status": "Open", "agreement_fulfilled": "Ongoing"} + filters = {"status": "Open", "agreement_status": "Ongoing"} if issue: filters = {"name": issue} @@ -358,13 +358,13 @@ def set_service_level_agreement_variance(issue=None): variance = round(time_diff_in_hours(doc.response_by, current_time), 2) frappe.db.set_value(dt="Issue", dn=doc.name, field="response_by_variance", val=variance, update_modified=False) if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) + frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) if not doc.resolution_date: # resolution_date set when issue has been closed variance = round(time_diff_in_hours(doc.resolution_by, current_time), 2) frappe.db.set_value(dt="Issue", dn=doc.name, field="resolution_by_variance", val=variance, update_modified=False) if variance < 0: - frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_fulfilled", val="Failed", update_modified=False) + frappe.db.set_value(dt="Issue", dn=doc.name, field="agreement_status", val="Failed", update_modified=False) def set_resolution_time(issue): diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py index fb8ceb53b2..c962dc6b31 100644 --- a/erpnext/support/doctype/issue/test_issue.py +++ b/erpnext/support/doctype/issue/test_issue.py @@ -73,7 +73,7 @@ class TestIssue(unittest.TestCase): issue.status = 'Closed' issue.save() - self.assertEqual(issue.agreement_fulfilled, 'Fulfilled') + self.assertEqual(issue.agreement_status, 'Fulfilled') def test_issue_metrics(self): creation = datetime.datetime(2020, 3, 4, 4, 0) diff --git a/erpnext/support/doctype/warranty_claim/warranty_claim.json b/erpnext/support/doctype/warranty_claim/warranty_claim.json index ae1a7a569d..88ee4a3beb 100644 --- a/erpnext/support/doctype/warranty_claim/warranty_claim.json +++ b/erpnext/support/doctype/warranty_claim/warranty_claim.json @@ -361,11 +361,11 @@ ], "icon": "fa fa-bug", "idx": 1, - "modified": "2019-05-24 10:56:30.626200", + "modified": "2020-09-18 17:26:09.703215", "modified_by": "Administrator", "module": "Support", "name": "Warranty Claim", - "owner": "harshada@webnotestech.com", + "owner": "Administrator", "permissions": [ { "create": 1, diff --git a/erpnext/support/report/first_response_time_for_issues/__init__.py b/erpnext/support/report/first_response_time_for_issues/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js new file mode 100644 index 0000000000..576e0b76da --- /dev/null +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.js @@ -0,0 +1,44 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["First Response Time for Issues"] = { + "filters": [ + { + "fieldname": "from_date", + "label": __("From Date"), + "fieldtype": "Date", + "reqd": 1, + "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) + }, + { + "fieldname": "to_date", + "label": __("To Date"), + "fieldtype": "Date", + "reqd": 1, + "default":frappe.datetime.nowdate() + } + ], + get_chart_data: function(_columns, result) { + return { + data: { + labels: result.map(d => d[0]), + datasets: [{ + name: 'First Response Time', + values: result.map(d => d[1]) + }] + }, + type: "line", + tooltipOptions: { + formatTooltipY: d => { + let duration_options = { + hide_days: 0, + hide_seconds: 0 + }; + value = frappe.utils.get_formatted_duration(d, duration_options); + return value; + } + } + } + } +}; diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json new file mode 100644 index 0000000000..c4fe6f5193 --- /dev/null +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.json @@ -0,0 +1,26 @@ +{ + "add_total_row": 0, + "creation": "2020-08-10 18:12:42.391224", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "letter_head": "Test 2", + "modified": "2020-08-10 18:12:42.391224", + "modified_by": "Administrator", + "module": "Support", + "name": "First Response Time for Issues", + "owner": "Administrator", + "prepared_report": 0, + "query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;", + "ref_doctype": "Issue", + "report_name": "First Response Time for Issues", + "report_type": "Script Report", + "roles": [ + { + "role": "Support Team" + } + ] +} \ No newline at end of file diff --git a/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py new file mode 100644 index 0000000000..922da2b33d --- /dev/null +++ b/erpnext/support/report/first_response_time_for_issues/first_response_time_for_issues.py @@ -0,0 +1,35 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe + +def execute(filters=None): + columns = [ + { + 'fieldname': 'creation_date', + 'label': 'Date', + 'fieldtype': 'Date', + 'width': 300 + }, + { + 'fieldname': 'first_response_time', + 'fieldtype': 'Duration', + 'label': 'First Response Time', + 'width': 300 + }, + ] + + data = frappe.db.sql(''' + SELECT + date(creation) as creation_date, + avg(first_response_time) as avg_response_time + FROM tabIssue + WHERE + date(creation) between %s and %s + and first_response_time > 0 + GROUP BY creation_date + ORDER BY creation_date desc + ''', (filters.from_date, filters.to_date)) + + return columns, data \ No newline at end of file diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js deleted file mode 100644 index 034e7779a6..0000000000 --- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.js +++ /dev/null @@ -1,30 +0,0 @@ -frappe.query_reports["Minutes to First Response for Issues"] = { - "filters": [ - { - "fieldname":"from_date", - "label": __("From Date"), - "fieldtype": "Date", - 'reqd': 1, - "default": frappe.datetime.add_days(frappe.datetime.nowdate(), -30) - }, - { - "fieldname":"to_date", - "label": __("To Date"), - "fieldtype": "Date", - 'reqd': 1, - "default":frappe.datetime.nowdate() - }, - ], - get_chart_data: function(columns, result) { - return { - data: { - labels: result.map(d => d[0]), - datasets: [{ - name: 'Mins to first response', - values: result.map(d => d[1]) - }] - }, - type: 'line', - } - } -} diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json deleted file mode 100644 index 539d3d941f..0000000000 --- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "add_total_row": 0, - "apply_user_permissions": 1, - "creation": "2016-06-14 17:44:26.034112", - "disabled": 0, - "docstatus": 0, - "doctype": "Report", - "idx": 2, - "is_standard": "Yes", - "modified": "2017-02-24 20:06:18.391100", - "modified_by": "Administrator", - "module": "Support", - "name": "Minutes to First Response for Issues", - "owner": "Administrator", - "query": "select date(creation) as creation_date, avg(mins_to_first_response) from tabIssue where creation > '2016-05-01' group by date(creation) order by creation_date;", - "ref_doctype": "Issue", - "report_name": "Minutes to First Response for Issues", - "report_type": "Script Report", - "roles": [ - { - "role": "Support Team" - } - ] -} \ No newline at end of file diff --git a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py b/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py deleted file mode 100644 index 57c2d442b2..0000000000 --- a/erpnext/support/report/minutes_to_first_response_for_issues/minutes_to_first_response_for_issues.py +++ /dev/null @@ -1,28 +0,0 @@ -# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors -# For license information, please see license.txt - -from __future__ import unicode_literals -import frappe - -def execute(filters=None): - columns = [ - { - 'fieldname': 'creation_date', - 'label': 'Date', - 'fieldtype': 'Date' - }, - { - 'fieldname': 'mins', - 'fieldtype': 'Float', - 'label': 'Mins to First Response' - }, - ] - - data = frappe.db.sql('''select date(creation) as creation_date, - avg(mins_to_first_response) as mins - from tabIssue - where date(creation) between %s and %s - and mins_to_first_response > 0 - group by creation_date order by creation_date desc''', (filters.from_date, filters.to_date)) - - return columns, data diff --git a/erpnext/utilities/activation.py b/erpnext/utilities/activation.py index 63c36b35d1..7b17c8c464 100644 --- a/erpnext/utilities/activation.py +++ b/erpnext/utilities/activation.py @@ -11,8 +11,39 @@ def get_level(): activation_level = 0 sales_data = [] min_count = 0 - doctypes = {"Item": 5, "Customer": 5, "Sales Order": 2, "Sales Invoice": 2, "Purchase Order": 2, "Employee": 3, "Lead": 3, "Quotation": 3, - "Payment Entry": 2, "User": 5, "Student": 5, "Instructor": 5, "BOM": 3, "Journal Entry": 3, "Stock Entry": 3} + doctypes = { + "Asset": 5, + "BOM": 3, + "Customer": 5, + "Delivery Note": 5, + "Employee": 3, + "Instructor": 5, + "Instructor": 5, + "Issue": 5, + "Item": 5, + "Journal Entry": 3, + "Lead": 3, + "Leave Application": 5, + "Material Request": 5, + "Opportunity": 5, + "Payment Entry": 2, + "Project": 5, + "Purchase Order": 2, + "Purchase Invoice": 5, + "Purchase Receipt": 5, + "Quotation": 3, + "Salary Slip": 5, + "Salary Structure": 5, + "Sales Order": 2, + "Sales Invoice": 2, + "Stock Entry": 3, + "Student": 5, + "Supplier": 5, + "Task": 5, + "User": 5, + "Work Order": 5 + } + for doctype, min_count in iteritems(doctypes): count = frappe.db.count(doctype) if count > min_count: diff --git a/erpnext/utilities/desk_page/utilities/utilities.json b/erpnext/utilities/desk_page/utilities/utilities.json new file mode 100644 index 0000000000..591eab5ed4 --- /dev/null +++ b/erpnext/utilities/desk_page/utilities/utilities.json @@ -0,0 +1,29 @@ +{ + "cards": [ + { + "hidden": 0, + "label": "Video", + "links": "[\n {\n \"description\": \"Video\",\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Video settings\",\n \"label\": \"Video Settings\",\n \"name\": \"Video Settings\",\n \"type\": \"doctype\"\n }\n]" + } + ], + "category": "Modules", + "charts": [], + "creation": "2020-09-10 12:21:22.335307", + "developer_mode_only": 0, + "disable_user_customization": 0, + "docstatus": 0, + "doctype": "Desk Page", + "extends_another_page": 0, + "hide_custom": 0, + "idx": 0, + "is_standard": 1, + "label": "Utilities", + "modified": "2020-09-10 12:33:30.089853", + "modified_by": "user@erpnext.com", + "module": "Utilities", + "name": "Utilities", + "owner": "user@erpnext.com", + "pin_to_bottom": 1, + "pin_to_top": 0, + "shortcuts": [] +} \ No newline at end of file diff --git a/erpnext/utilities/doctype/video/video.js b/erpnext/utilities/doctype/video/video.js index 056bd3ccd6..9cb5a155ad 100644 --- a/erpnext/utilities/doctype/video/video.js +++ b/erpnext/utilities/doctype/video/video.js @@ -2,7 +2,16 @@ // For license information, please see license.txt frappe.ui.form.on('Video', { - // refresh: function(frm) { + refresh: function (frm) { + frm.events.toggle_youtube_statistics_section(frm); + frm.add_custom_button("Watch Video", () => frappe.help.show_video(frm.doc.url, frm.doc.title)); + }, - // } + toggle_youtube_statistics_section: (frm) => { + if (frm.doc.provider === "YouTube") { + frappe.db.get_single_value("Video Settings", "enable_youtube_tracking").then( val => { + frm.toggle_display("youtube_tracking_section", val); + }); + } + } }); diff --git a/erpnext/utilities/doctype/video/video.json b/erpnext/utilities/doctype/video/video.json index 5d2cc13348..2a82db2514 100644 --- a/erpnext/utilities/doctype/video/video.json +++ b/erpnext/utilities/doctype/video/video.json @@ -11,11 +11,19 @@ "title", "provider", "url", + "youtube_video_id", "column_break_4", "publish_date", "duration", + "youtube_tracking_section", + "like_count", + "view_count", + "col_break", + "dislike_count", + "comment_count", "section_break_7", - "description" + "description", + "image" ], "fields": [ { @@ -37,7 +45,6 @@ { "fieldname": "url", "fieldtype": "Data", - "in_list_view": 1, "label": "URL", "reqd": 1 }, @@ -48,11 +55,12 @@ { "fieldname": "publish_date", "fieldtype": "Date", + "in_list_view": 1, "label": "Publish Date" }, { "fieldname": "duration", - "fieldtype": "Data", + "fieldtype": "Duration", "label": "Duration" }, { @@ -62,13 +70,67 @@ { "fieldname": "description", "fieldtype": "Text Editor", - "in_list_view": 1, "label": "Description", "reqd": 1 + }, + { + "fieldname": "like_count", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Likes", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "view_count", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Views", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "col_break", + "fieldtype": "Column Break" + }, + { + "fieldname": "dislike_count", + "fieldtype": "Float", + "label": "Dislikes", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "comment_count", + "fieldtype": "Float", + "label": "Comments", + "no_copy": 1, + "read_only": 1 + }, + { + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "label": "Image", + "no_copy": 1 + }, + { + "depends_on": "eval:doc.provider==\"YouTube\"", + "fieldname": "youtube_tracking_section", + "fieldtype": "Section Break", + "label": "Youtube Statistics" + }, + { + "fieldname": "youtube_video_id", + "fieldtype": "Data", + "hidden": 1, + "label": "Youtube ID", + "read_only": 1 } ], + "image_field": "image", "links": [], - "modified": "2020-07-21 19:29:46.603734", + "modified": "2020-09-07 17:02:20.185794", "modified_by": "Administrator", "module": "Utilities", "name": "Video", diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 3c17b560f3..c2e414eef8 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -3,8 +3,144 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +import re +import pytz from frappe.model.document import Document +from frappe import _ +from datetime import datetime +from six import string_types +from pyyoutube import Api class Video(Document): - pass + def validate(self): + if self.provider == "YouTube" and is_tracking_enabled(): + self.set_video_id() + self.set_youtube_statistics() + + def set_video_id(self): + if self.url and not self.get("youtube_video_id"): + self.youtube_video_id = get_id_from_url(self.url) + + def set_youtube_statistics(self): + api_key = frappe.db.get_single_value("Video Settings", "api_key") + api = Api(api_key=api_key) + + try: + video = api.get_video_by_id(video_id=self.youtube_video_id) + video_stats = video.items[0].to_dict().get('statistics') + + self.like_count = video_stats.get('likeCount') + self.view_count = video_stats.get('viewCount') + self.dislike_count = video_stats.get('dislikeCount') + self.comment_count = video_stats.get('commentCount') + + except Exception: + title = "Failed to Update YouTube Statistics for Video: {0}".format(self.name) + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + +def is_tracking_enabled(): + return frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") + + +def get_frequency(value): + # Return numeric value from frequency field, return 1 as fallback default value: 1 hour + if value != "Daily": + return frappe.utils.cint(value[:2].strip()) + elif value: + return 24 + return 1 + + +def update_youtube_data(): + # Called every 30 minutes via hooks + enable_youtube_tracking, frequency = frappe.db.get_value("Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"]) + + if not enable_youtube_tracking: + return + + frequency = get_frequency(frequency) + time = datetime.now() + timezone = pytz.timezone(frappe.utils.get_time_zone()) + site_time = time.astimezone(timezone) + + if frequency == 30: + batch_update_youtube_data() + elif site_time.hour % frequency == 0 and site_time.minute < 15: + # make sure it runs within the first 15 mins of the hour + batch_update_youtube_data() + + +def get_formatted_ids(video_list): + # format ids to comma separated string for bulk request + ids = [] + for video in video_list: + ids.append(video.youtube_video_id) + + return ','.join(ids) + + +@frappe.whitelist() +def get_id_from_url(url): + """ + Returns video id from url + :param youtube url: String URL + """ + if not isinstance(url, string_types): + frappe.throw(_("URL can only be a string"), title=_("Invalid URL")) + + pattern = re.compile(r'[a-z\:\//\.]+(youtube|youtu)\.(com|be)/(watch\?v=|embed/|.+\?v=)?([^"&?\s]{11})?') + id = pattern.match(url) + return id.groups()[-1] + + +@frappe.whitelist() +def batch_update_youtube_data(): + def get_youtube_statistics(video_ids): + api_key = frappe.db.get_single_value("Video Settings", "api_key") + api = Api(api_key=api_key) + try: + video = api.get_video_by_id(video_id=video_ids) + video_stats = video.items + return video_stats + except Exception: + title = "Failed to Update YouTube Statistics" + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + + def prepare_and_set_data(video_list): + video_ids = get_formatted_ids(video_list) + stats = get_youtube_statistics(video_ids) + set_youtube_data(stats) + + def set_youtube_data(entries): + for entry in entries: + video_stats = entry.to_dict().get('statistics') + video_id = entry.to_dict().get('id') + stats = { + 'like_count' : video_stats.get('likeCount'), + 'view_count' : video_stats.get('viewCount'), + 'dislike_count' : video_stats.get('dislikeCount'), + 'comment_count' : video_stats.get('commentCount'), + 'video_id': video_id + } + + frappe.db.sql(""" + UPDATE `tabVideo` + SET + like_count = %(like_count)s, + view_count = %(view_count)s, + dislike_count = %(dislike_count)s, + comment_count = %(comment_count)s + WHERE youtube_video_id = %(video_id)s""", stats) + + video_list = frappe.get_all("Video", fields=["youtube_video_id"]) + if len(video_list) > 50: + # Update in batches of 50 + start, end = 0, 50 + while start < len(video_list): + batch = video_list[start:end] + prepare_and_set_data(batch) + start += 50 + end += 50 + else: + prepare_and_set_data(video_list) diff --git a/erpnext/utilities/doctype/video/video_list.js b/erpnext/utilities/doctype/video/video_list.js new file mode 100644 index 0000000000..8273a4a781 --- /dev/null +++ b/erpnext/utilities/doctype/video/video_list.js @@ -0,0 +1,7 @@ +frappe.listview_settings["Video"] = { + onload: (listview) => { + listview.page.add_menu_item(__("Video Settings"), function() { + frappe.set_route("Form","Video Settings", "Video Settings"); + }); + } +} \ No newline at end of file diff --git a/erpnext/utilities/doctype/video_settings/__init__.py b/erpnext/utilities/doctype/video_settings/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/utilities/doctype/video_settings/test_video_settings.py b/erpnext/utilities/doctype/video_settings/test_video_settings.py new file mode 100644 index 0000000000..b217afe3d8 --- /dev/null +++ b/erpnext/utilities/doctype/video_settings/test_video_settings.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors +# See license.txt +from __future__ import unicode_literals + +# import frappe +import unittest + +class TestVideoSettings(unittest.TestCase): + pass diff --git a/erpnext/utilities/doctype/video_settings/video_settings.js b/erpnext/utilities/doctype/video_settings/video_settings.js new file mode 100644 index 0000000000..9ac8b9ec16 --- /dev/null +++ b/erpnext/utilities/doctype/video_settings/video_settings.js @@ -0,0 +1,8 @@ +// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Video Settings', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/utilities/doctype/video_settings/video_settings.json b/erpnext/utilities/doctype/video_settings/video_settings.json new file mode 100644 index 0000000000..fb3274decd --- /dev/null +++ b/erpnext/utilities/doctype/video_settings/video_settings.json @@ -0,0 +1,60 @@ +{ + "actions": [], + "creation": "2020-08-02 03:50:21.339609", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "enable_youtube_tracking", + "api_key", + "frequency" + ], + "fields": [ + { + "default": "0", + "fieldname": "enable_youtube_tracking", + "fieldtype": "Check", + "label": "Enable YouTube Tracking" + }, + { + "depends_on": "eval:doc.enable_youtube_tracking", + "fieldname": "api_key", + "fieldtype": "Data", + "label": "API Key", + "mandatory_depends_on": "eval:doc.enable_youtube_tracking" + }, + { + "default": "1 hr", + "depends_on": "eval:doc.enable_youtube_tracking", + "fieldname": "frequency", + "fieldtype": "Select", + "label": "Frequency", + "mandatory_depends_on": "eval:doc.enable_youtube_tracking", + "options": "30 mins\n1 hr\n6 hrs\nDaily" + } + ], + "index_web_pages_for_search": 1, + "issingle": 1, + "links": [], + "modified": "2020-09-07 16:09:00.360668", + "modified_by": "Administrator", + "module": "Utilities", + "name": "Video Settings", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "quick_entry": 1, + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/utilities/doctype/video_settings/video_settings.py b/erpnext/utilities/doctype/video_settings/video_settings.py new file mode 100644 index 0000000000..36fb54f015 --- /dev/null +++ b/erpnext/utilities/doctype/video_settings/video_settings.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.model.document import Document +from apiclient.discovery import build + +class VideoSettings(Document): + def validate(self): + self.validate_youtube_api_key() + + def validate_youtube_api_key(self): + if self.enable_youtube_tracking and self.api_key: + try: + build("youtube", "v3", developerKey=self.api_key) + except Exception: + title = _("Failed to Authenticate the API key.") + frappe.log_error(title + "\n\n" + frappe.get_traceback(), title=title) + frappe.throw(title + " Please check the error logs.", title=_("Invalid Credentials")) \ No newline at end of file diff --git a/erpnext/utilities/report/__init__.py b/erpnext/utilities/report/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/utilities/report/youtube_interactions/__init__.py b/erpnext/utilities/report/youtube_interactions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.js b/erpnext/utilities/report/youtube_interactions/youtube_interactions.js new file mode 100644 index 0000000000..6e3e4e6980 --- /dev/null +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.js @@ -0,0 +1,20 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +frappe.query_reports["YouTube Interactions"] = { + "filters": [ + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.now_date(), -12), + }, + { + fieldname:"to_date", + label: __("To Date"), + fieldtype: "Date", + default: frappe.datetime.now_date(), + } + ] +}; diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.json b/erpnext/utilities/report/youtube_interactions/youtube_interactions.json new file mode 100644 index 0000000000..a40247b6df --- /dev/null +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.json @@ -0,0 +1,27 @@ +{ + "add_total_row": 0, + "creation": "2020-08-02 05:05:00.457093", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "idx": 0, + "is_standard": "Yes", + "modified": "2020-08-02 05:05:00.457093", + "modified_by": "Administrator", + "module": "Utilities", + "name": "YouTube Interactions", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Video", + "report_name": "YouTube Interactions", + "report_type": "Script Report", + "roles": [ + { + "role": "All" + }, + { + "role": "System Manager" + } + ] +} \ No newline at end of file diff --git a/erpnext/utilities/report/youtube_interactions/youtube_interactions.py b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py new file mode 100644 index 0000000000..3516a35097 --- /dev/null +++ b/erpnext/utilities/report/youtube_interactions/youtube_interactions.py @@ -0,0 +1,113 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +from __future__ import unicode_literals +import frappe +from frappe import _ +from frappe.utils import flt + +def execute(filters=None): + if not frappe.db.get_single_value("Video Settings", "enable_youtube_tracking") or not filters: + return [], [] + + columns = get_columns() + data = get_data(filters) + chart_data, summary = get_chart_summary_data(data) + return columns, data, None, chart_data, summary + +def get_columns(): + return [ + { + "label": _("Published Date"), + "fieldname": "publish_date", + "fieldtype": "Date", + "width": 100 + }, + { + "label": _("Title"), + "fieldname": "title", + "fieldtype": "Data", + "width": 200 + }, + { + "label": _("Duration"), + "fieldname": "duration", + "fieldtype": "Duration", + "width": 100 + }, + { + "label": _("Views"), + "fieldname": "view_count", + "fieldtype": "Float", + "width": 200 + }, + { + "label": _("Likes"), + "fieldname": "like_count", + "fieldtype": "Float", + "width": 200 + }, + { + "label": _("Dislikes"), + "fieldname": "dislike_count", + "fieldtype": "Float", + "width": 100 + }, + { + "label": _("Comments"), + "fieldname": "comment_count", + "fieldtype": "Float", + "width": 100 + } + ] + +def get_data(filters): + return frappe.db.sql(""" + SELECT + publish_date, title, provider, duration, + view_count, like_count, dislike_count, comment_count + FROM `tabVideo` + WHERE view_count is not null + and publish_date between %(from_date)s and %(to_date)s + ORDER BY view_count desc""", filters, as_dict=1) + +def get_chart_summary_data(data): + labels, likes, views = [], [], [] + total_views = 0 + + for row in data: + labels.append(row.get('title')) + likes.append(row.get('like_count')) + views.append(row.get('view_count')) + total_views += flt(row.get('view_count')) + + + chart_data = { + "data" : { + "labels" : labels, + "datasets" : [ + { + "name" : "Likes", + "values" : likes + }, + { + "name" : "Views", + "values" : views + } + ] + }, + "type": "bar", + "barOptions": { + "stacked": 1 + }, + } + + summary = [ + { + "value": total_views, + "indicator": "Blue", + "label": "Total Views", + "datatype": "Float", + } + ] + return chart_data, summary \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 912d61f7a6..c4f9171fca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,10 +3,11 @@ frappe gocardless-pro==1.11.0 googlemaps==3.1.1 pandas==1.0.5 -plaid-python==3.4.0 +plaid-python==6.0.0 pycountry==19.8.18 PyGithub==1.44.1 python-stdnum==1.12 +python-youtube==0.6.0 taxjar==1.9.0 tweepy==3.8.0 Unidecode==1.1.1