diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index fbd4dee4d66..637aa80f8a6 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -356,6 +356,7 @@
"fieldname": "bill_date",
"fieldtype": "Date",
"label": "Supplier Invoice Date",
+ "no_copy": 1,
"oldfieldname": "bill_date",
"oldfieldtype": "Date",
"print_hide": 1
@@ -1307,7 +1308,7 @@
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
- "modified": "2020-08-20 11:08:19.611710",
+ "modified": "2020-09-21 12:22:09.164068",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
index efc76f9158b..97035278754 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 a49cc321495..65d13749d32 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js
@@ -69,7 +69,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 04fc33220d9..f6632fa2632 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/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index a53ff881777..04ef68357f2 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));
@@ -437,6 +430,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 2a9e4db32b9..13fb2665982 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 \
@@ -84,6 +84,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")
@@ -308,7 +313,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 e8def53c070..d914dabc123 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -374,19 +374,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)
@@ -395,9 +394,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`
@@ -469,8 +468,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 c80f95e1555..79fcb957d4d 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 c3755a3fb9a..cddee5fa0f1 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 075db6e46ba..8427e66a868 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -17,6 +17,8 @@ from erpnext.stock.doctype.material_request.material_request import make_purchas
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.controllers.status_updater import OverAllowanceError
+from erpnext.stock.doctype.batch.test_batch import make_new_batch
+from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
@@ -200,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_qty(self):
po = create_purchase_order()
@@ -580,7 +629,7 @@ class TestPurchaseOrder(unittest.TestCase):
def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 1"
- make_subcontracted_item(item_code)
+ make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
@@ -602,7 +651,7 @@ class TestPurchaseOrder(unittest.TestCase):
def test_backflush_based_on_stock_entry(self):
item_code = "_Test Subcontracted FG Item 1"
- make_subcontracted_item(item_code)
+ make_subcontracted_item(item_code=item_code)
make_item('Sub Contracted Raw Material 1', {
'is_stock_item': 1,
'is_sub_contracted_item': 1
@@ -661,6 +710,76 @@ class TestPurchaseOrder(unittest.TestCase):
update_backflush_based_on("BOM")
+ def test_backflushed_based_on_for_multiple_batches(self):
+ item_code = "_Test Subcontracted FG Item 2"
+ make_item('Sub Contracted Raw Material 2', {
+ 'is_stock_item': 1,
+ 'is_sub_contracted_item': 1
+ })
+
+ make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
+ raw_materials=["Sub Contracted Raw Material 2"])
+
+ update_backflush_based_on("Material Transferred for Subcontract")
+
+ order_qty = 500
+ po = create_purchase_order(item_code=item_code, qty=order_qty,
+ is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
+
+ make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
+
+ rm_items = [
+ {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
+ "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
+
+ rm_item_string = json.dumps(rm_items)
+ se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
+ se.submit()
+
+ for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
+ make_new_batch(batch_id=batch, item_code=item_code)
+
+ pr = make_purchase_receipt(po.name)
+
+ # partial receipt
+ pr.get('items')[0].qty = 30
+ pr.get('items')[0].batch_no = "ABCD1"
+
+ purchase_order = po.name
+ purchase_order_item = po.items[0].name
+
+ for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
+ pr.append("items", {
+ "item_code": pr.get('items')[0].item_code,
+ "item_name": pr.get('items')[0].item_name,
+ "uom": pr.get('items')[0].uom,
+ "stock_uom": pr.get('items')[0].stock_uom,
+ "warehouse": pr.get('items')[0].warehouse,
+ "conversion_factor": pr.get('items')[0].conversion_factor,
+ "cost_center": pr.get('items')[0].cost_center,
+ "rate": pr.get('items')[0].rate,
+ "qty": qty,
+ "batch_no": batch_no,
+ "purchase_order": purchase_order,
+ "purchase_order_item": purchase_order_item
+ })
+
+ pr.submit()
+
+ pr1 = make_purchase_receipt(po.name)
+ pr1.get('items')[0].qty = 300
+ pr1.get('items')[0].batch_no = "ABCD1"
+ pr1.save()
+
+ pr_key = ("Sub Contracted Raw Material 2", po.name)
+ consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
+
+ self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
+ self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
+
+ update_backflush_based_on("BOM")
+
def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
frappe.db.set_value("Accounts Settings", "Accounts Settings",
@@ -712,27 +831,33 @@ def make_pr_against_po(po, received_qty=0):
pr.submit()
return pr
-def make_subcontracted_item(item_code):
+def make_subcontracted_item(**args):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
- if not frappe.db.exists('Item', item_code):
- make_item(item_code, {
+ args = frappe._dict(args)
+
+ if not frappe.db.exists('Item', args.item_code):
+ make_item(args.item_code, {
'is_stock_item': 1,
- 'is_sub_contracted_item': 1
+ 'is_sub_contracted_item': 1,
+ 'has_batch_no': args.get("has_batch_no") or 0
})
- if not frappe.db.exists('Item', "Test Extra Item 1"):
- make_item("Test Extra Item 1", {
- 'is_stock_item': 1,
- })
+ if not args.raw_materials:
+ if not frappe.db.exists('Item', "Test Extra Item 1"):
+ make_item("Test Extra Item 1", {
+ 'is_stock_item': 1,
+ })
- if not frappe.db.exists('Item', "Test Extra Item 2"):
- make_item("Test Extra Item 2", {
- 'is_stock_item': 1,
- })
+ if not frappe.db.exists('Item', "Test Extra Item 2"):
+ make_item("Test Extra Item 2", {
+ 'is_stock_item': 1,
+ })
- if not frappe.db.get_value('BOM', {'item': item_code}, 'name'):
- make_bom(item = item_code, raw_materials = ['_Test FG Item', 'Test Extra Item 1'])
+ args.raw_materials = ['_Test FG Item', 'Test Extra Item 1']
+
+ if not frappe.db.get_value('BOM', {'item': args.item_code}, 'name'):
+ make_bom(item = args.item_code, raw_materials = args.get("raw_materials"))
def update_backflush_based_on(based_on):
doc = frappe.get_doc('Buying Settings')
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index fc535074005..65d815a9094 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")
@@ -1127,6 +1127,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
@@ -1140,6 +1152,7 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname,
child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
child_item.uom = item.stock_uom
+ 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.")
@@ -1162,6 +1175,7 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.uom = item.stock_uom
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):
@@ -1199,7 +1213,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:
@@ -1234,7 +1248,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)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 64bca44a608..6e05a312352 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _, msgprint
from frappe.utils import flt,cint, cstr, getdate
-
+from six import iteritems
from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
@@ -289,10 +289,10 @@ class BuyingController(StockController):
title=_("Limit Crossed"))
transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
- backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
+ # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
for raw_material in transferred_raw_materials + non_stock_items:
- rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order)
+ rm_item_key = (raw_material.rm_item_code, item.purchase_order)
raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
consumed_qty = raw_material_data.get('qty', 0)
@@ -321,6 +321,8 @@ class BuyingController(StockController):
set_serial_nos(raw_material, consumed_serial_nos, qty)
if raw_material.batch_nos:
+ backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
+
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty:
@@ -337,6 +339,7 @@ class BuyingController(StockController):
if not rm.main_item_code:
rm.main_item_code = fg_item_doc.item_code
+ rm.reference_name = fg_item_doc.name
rm.required_qty = qty
rm.consumed_qty = qty
@@ -864,39 +867,49 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
return raw_materials
def get_backflushed_subcontracted_raw_materials(purchase_orders):
- common_query = """
- SELECT
- CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key,
- SUM(prsi.consumed_qty) AS qty,
- {serial_no_concat_syntax} AS serial_nos,
- {batch_no_concat_syntax} AS batch_nos
- FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi
- WHERE
- pr.name = pri.parent
- AND pr.name = prsi.parent
- AND pri.purchase_order IN %s
- AND pri.item_code = prsi.main_item_code
- AND pr.docstatus = 1
- GROUP BY prsi.rm_item_code, pri.purchase_order
- """
+ purchase_receipts = frappe.get_all("Purchase Receipt Item",
+ fields = ["purchase_order", "item_code", "name", "parent"],
+ filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
- backflushed_raw_materials = frappe.db.multisql({
- 'mariadb': common_query.format(
- serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)",
- batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)"
- ),
- 'postgres': common_query.format(
- serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')",
- batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')"
- )
- }, (purchase_orders, ), as_dict=1)
+ distinct_purchase_receipts = {}
+ for pr in purchase_receipts:
+ key = (pr.purchase_order, pr.item_code, pr.parent)
+ distinct_purchase_receipts.setdefault(key, []).append(pr.name)
backflushed_raw_materials_map = frappe._dict()
- for item in backflushed_raw_materials:
- backflushed_raw_materials_map.setdefault(item.item_key, item)
+ for args, references in iteritems(distinct_purchase_receipts):
+ purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
+
+ for data in purchase_receipt_supplied_items:
+ pr_key = (data.rm_item_code, args[0])
+ if pr_key not in backflushed_raw_materials_map:
+ backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
+ "qty": 0.0,
+ "serial_no": [],
+ "batch_no": [],
+ "consumed_batch": {}
+ }))
+
+ row = backflushed_raw_materials_map.get(pr_key)
+ row.qty += data.consumed_qty
+
+ for field in ["serial_no", "batch_no"]:
+ if data.get(field):
+ row[field].append(data.get(field))
+
+ if data.get("batch_no"):
+ if data.get("batch_no") in row.consumed_batch:
+ row.consumed_batch[data.get("batch_no")] += data.consumed_qty
+ else:
+ row.consumed_batch[data.get("batch_no")] = data.consumed_qty
return backflushed_raw_materials_map
+def get_supplied_items(item_code, purchase_receipt, references):
+ return frappe.get_all("Purchase Receipt Item Supplied",
+ fields=["rm_item_code", "consumed_qty", "serial_no", "batch_no"],
+ filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
+
def get_asset_item_details(asset_items):
asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@@ -1034,14 +1047,12 @@ def get_backflushed_batch_qty_map(purchase_order, fg_item):
return backflushed_batch_qty_map
-def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map, po):
+def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
# Returns available batches to be backflushed based on requirements
transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
if not transferred_batches:
transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
- backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {})
-
available_batches = []
for (batch, transferred_qty) in transferred_batches.items():
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index 532e19cffd9..a033a2a722d 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 0ffbb877ea7..22a4004955f 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 d8203d7390f..27062172239 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 81fb9843f61..3afccf95b8e 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 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:
@@ -148,10 +156,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
@@ -169,6 +177,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
return transactions
+
def new_bank_transaction(transaction):
result = []
@@ -183,8 +192,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:
@@ -217,6 +226,7 @@ def new_bank_transaction(transaction):
return result
+
def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
@@ -224,4 +234,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 29e8fa4fec8..9f0bb92f537 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/patches/v12_0/move_plaid_settings_to_doctype.py b/erpnext/patches/v12_0/move_plaid_settings_to_doctype.py
index 8e60d33f850..d2bcb12070c 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/public/js/utils/party.js b/erpnext/public/js/utils/party.js
index 065326744c2..af1f4335148 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: {
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 254f3c1993a..31c328fdf3e 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -418,15 +418,16 @@ class TestSalesOrder(unittest.TestCase):
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
test_user.remove_roles("Accounts User")
frappe.set_user("Administrator")
-
+
def test_update_child_qty_rate_with_workflow(self):
from frappe.model.workflow import apply_workflow
+ frappe.set_user("Administrator")
workflow = make_sales_order_workflow()
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
+ frappe.set_user("Administrator")
apply_workflow(so, 'Approve')
- frappe.set_user("Administrator")
user = 'test@example.com'
test_user = frappe.get_doc('User', user)
test_user.add_roles("Sales User", "Test Junior Approver")
@@ -1009,7 +1010,7 @@ def make_sales_order_workflow():
"is_active": 1,
"send_email_alert": 0,
})
- workflow.append('states', dict( state = 'Pending', allow_edit = 'All' ))
+ workflow.append('states', dict( state = 'Pending', allow_edit = 'Administrator' ))
workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 ))
workflow.append('transitions', dict(
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1,
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js
index 30790f0a987..f1728b7afda 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.js
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.js
@@ -449,8 +449,7 @@ erpnext.pos.PointOfSale = class PointOfSale {
$(this.frm.msgbox.body).find('.btn-primary').on('click', () => {
this.frm.msgbox.hide();
- const frm = this.events.get_frm();
- frm.doc = this.doc;
+ const frm = this.frm;
frm.print_preview.lang_code = frm.doc.language;
frm.print_preview.printit(true);
});
@@ -688,8 +687,8 @@ erpnext.pos.PointOfSale = class PointOfSale {
if(this.frm.doc.docstatus != 1 ){
await this.frm.save();
}
- const frm = this.events.get_frm();
- frm.doc = this.doc;
+
+ const frm = this.frm;
frm.print_preview.lang_code = frm.doc.language;
frm.print_preview.printit(true);
});
@@ -948,8 +947,12 @@ class POSCart {
}
},
onchange: () => {
- this.events.on_customer_change(this.customer_field.get_value());
- this.events.get_loyalty_details();
+ let customer = this.customer_field.get_value();
+ frappe.db.get_value("Customer", customer, "language", (r) => {
+ this.frm.doc.language = r ? r.language : "en-US";
+ this.events.on_customer_change(customer);
+ this.events.get_loyalty_details();
+ });
}
},
parent: this.wrapper.find('.customer-field'),
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 32445a618d1..0cc0fd487d3 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -241,3 +241,18 @@ class TestBatch(unittest.TestCase):
batch.insert()
return batch
+
+def make_new_batch(**args):
+ args = frappe._dict(args)
+
+ try:
+ batch = frappe.get_doc({
+ "doctype": "Batch",
+ "batch_id": args.batch_id,
+ "item": args.item_code,
+ }).insert()
+
+ except frappe.DuplicateEntryError:
+ batch = frappe.get_doc("Batch", args.batch_id)
+
+ return batch
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index d5e39786e65..7c244ea5023 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -152,7 +152,7 @@ class TestPurchaseReceipt(unittest.TestCase):
update_backflush_based_on("Material Transferred for Subcontract")
item_code = "_Test Subcontracted FG Item 1"
- make_subcontracted_item(item_code)
+ make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
@@ -471,8 +471,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/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index bbdac992b58..f8885a91edc 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -420,6 +420,9 @@ def get_item_details(item_code):
from tabItem where name=%s""", item_code, as_dict=True)[0]
def get_serial_nos(serial_no):
+ if isinstance(serial_no, list):
+ return serial_no
+
return [s.strip() for s in cstr(serial_no).strip().upper().replace(',', '\n').split('\n')
if s.strip()]
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 529602f2b9c..518f87fbe2a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -24,6 +24,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/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 170a62c8846..ca59e67a676 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -165,9 +165,12 @@ class StockReconciliation(StockController):
validate_is_stock_item(item_code, item.is_stock_item, verbose=0)
# item should not be serialized
- if item.has_serial_no and not row.serial_no and not item.serial_no_series:
+ if item.has_serial_no and not row.serial_no and not item.serial_no_series and flt(row.qty) > 0:
raise frappe.ValidationError(_("Serial no(s) required for serialized item {0}").format(item_code))
+ if flt(row.qty) == 0 and row.serial_no:
+ row.serial_no = ''
+
# item managed batch-wise not allowed
if item.has_batch_no and not row.batch_no and not item.create_new_batch:
raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code))
@@ -235,7 +238,7 @@ class StockReconciliation(StockController):
sl_entries = self.merge_similar_item_serial_nos(sl_entries)
def issue_existing_serial_and_batch(self, sl_entries):
- from erpnext.stock.stock_ledger import get_previous_sle
+ from erpnext.stock.stock_ledger import get_stock_ledger_entries
for row in self.items:
serial_nos = get_serial_nos(row.serial_no) or []
@@ -261,12 +264,14 @@ class StockReconciliation(StockController):
for serial_no in serial_nos:
args = self.get_sle_for_items(row, [serial_no])
- previous_sle = get_previous_sle({
+ previous_sle = get_stock_ledger_entries({
"item_code": row.item_code,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"serial_no": serial_no
- })
+ }, "<", "desc", "limit 1")
+
+ previous_sle = previous_sle and previous_sle[0] or {}
if previous_sle and row.warehouse != previous_sle.get("warehouse"):
# If serial no exists in different warehouse
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index ba885c09487..5c4bba730e3 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -460,7 +460,13 @@ def get_stock_ledger_entries(previous_sle, operator=None,
conditions += " and " + previous_sle.get("warehouse_condition")
if check_serial_no and previous_sle.get("serial_no"):
- conditions += " and serial_no like {}".format(frappe.db.escape('%{0}%'.format(previous_sle.get("serial_no"))))
+ serial_no = previous_sle.get("serial_no")
+ conditions += """ and (
+ serial_no = '{0}'
+ OR serial_no like '{0}\n%%'
+ OR serial_no like '%%\n{0}'
+ OR serial_no like '%%\n{0}\n%%'
+ ) and actual_qty > 0""".format(serial_no)
if not previous_sle.get("posting_date"):
previous_sle["posting_date"] = "1900-01-01"
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 95ecb7fecd7..db39bae8a63 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -96,11 +96,7 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
if with_valuation_rate:
if with_serial_no:
- serial_nos = last_entry.get("serial_no")
-
- if (serial_nos and
- len(get_serial_nos_data(serial_nos)) < last_entry.qty_after_transaction):
- serial_nos = get_serial_nos_data_after_transactions(args)
+ serial_nos = get_serial_nos_data_after_transactions(args)
return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
if last_entry else (0.0, 0.0, 0.0))
diff --git a/requirements.txt b/requirements.txt
index c277545fab5..f807fa6c29d 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -3,7 +3,7 @@ frappe
gocardless-pro==1.11.0
googlemaps==3.1.1
pandas==0.24.2
-plaid-python==3.4.0
+plaid-python==6.0.0
PyGithub==1.44.1
python-stdnum==1.12
Unidecode==1.1.1