diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py
index 1318cf18d76..b8f1b85907f 100644
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py
+++ b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py
@@ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document):
def populate_payment_entries(self):
if self.bank_statement is None: return
- filename = self.bank_statement.split("/")[-1]
+ file_url = self.bank_statement
if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0):
frappe.throw(_("Transactions already retreived from the statement"))
@@ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document):
if self.bank_settings:
mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items
statement_headers = self.get_statement_headers()
- transactions = get_transaction_entries(filename, statement_headers)
+ transactions = get_transaction_entries(file_url, statement_headers)
for entry in transactions:
date = entry[statement_headers["Date"]].strip()
#print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"]))
@@ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row):
transaction[header] = ""
return transaction
-def get_transaction_entries(filename, headers):
+def get_transaction_entries(file_url, headers):
header_index = {}
rows, transactions = [], []
- if (filename.lower().endswith("xlsx")):
+ if (file_url.lower().endswith("xlsx")):
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
- rows = read_xlsx_file_from_attached_file(file_id=filename)
- elif (filename.lower().endswith("csv")):
+ rows = read_xlsx_file_from_attached_file(file_url=file_url)
+ elif (file_url.lower().endswith("csv")):
from frappe.utils.csvutils import read_csv_content
- _file = frappe.get_doc("File", {"file_name": filename})
+ _file = frappe.get_doc("File", {"file_url": file_url})
filepath = _file.get_full_path()
with open(filepath,'rb') as csvfile:
rows = read_csv_content(csvfile.read())
- elif (filename.lower().endswith("xls")):
+ elif (file_url.lower().endswith("xls")):
+ filename = file_url.split("/")[-1]
rows = get_rows_from_xls_file(filename)
else:
frappe.throw(_("Only .csv and .xlsx files are supported currently"))
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index b854f27968f..4d19a7c4537 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -1019,7 +1019,7 @@ def make_inter_company_journal_entry(name, voucher_type, company):
return journal_entry.as_dict()
@frappe.whitelist()
-def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions=False):
+def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
def update_accounts(source, target, source_parent):
@@ -1045,6 +1045,6 @@ def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions=
},
"postprocess": update_accounts,
},
- }, target_doc, ignore_permissions=ignore_permissions)
+ }, target_doc)
return doclist
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 70c485133f0..3883637e363 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: {
@@ -46,6 +51,7 @@ frappe.ui.form.on('Payment Entry', {
}
}
});
+
frm.set_query("contact_person", function() {
if (frm.doc.party) {
return {
@@ -57,10 +63,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],
@@ -149,6 +157,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.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index c17f775a4e7..d1630f3ea69 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -84,7 +84,7 @@ class PaymentEntry(AccountsController):
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_payment_req_status()
- self.set_status()
+ self.set_status(update=True)
def set_payment_req_status(self):
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
@@ -279,7 +279,7 @@ class PaymentEntry(AccountsController):
outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"])
if outstanding_amount <= 0 and not is_return:
no_oustanding_refs.setdefault(d.reference_doctype, []).append(d)
-
+
for k, v in no_oustanding_refs.items():
frappe.msgprint(_("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.
\
If this is undesirable please cancel the corresponding Payment Entry.")
@@ -340,7 +340,7 @@ class PaymentEntry(AccountsController):
frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s
WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
- def set_status(self):
+ def set_status(self, update=False):
if self.docstatus == 2:
self.status = 'Cancelled'
elif self.docstatus == 1:
@@ -348,6 +348,9 @@ class PaymentEntry(AccountsController):
else:
self.status = 'Draft'
+ if update:
+ self.db_set('status', self.status)
+
def set_amounts(self):
self.set_amounts_in_company_currency()
self.set_total_allocated_amount()
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/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index 32b97ba80b5..c17f3aeb846 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -1,5 +1,4 @@
{
- "actions": [],
"autoname": "ACC-SUB-.YYYY.-.#####",
"creation": "2017-07-18 17:50:43.967266",
"doctype": "DocType",
@@ -184,7 +183,8 @@
"fieldname": "invoices",
"fieldtype": "Table",
"label": "Invoices",
- "options": "Subscription Invoice"
+ "options": "Subscription Invoice",
+ "read_only": 1
},
{
"collapsible": 1,
@@ -197,8 +197,7 @@
"fieldtype": "Column Break"
}
],
- "links": [],
- "modified": "2020-01-27 14:37:32.845173",
+ "modified": "2020-08-27 23:30:02.504042",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 98d07c71a3a..bc34816e375 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -326,8 +326,7 @@ class Subscription(Document):
def is_postpaid_to_invoice(self):
return getdate(nowdate()) > getdate(self.current_invoice_end) or \
- (getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and \
- not self.has_outstanding_invoice()
+ (getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start))
def is_prepaid_to_invoice(self):
if not self.generate_invoice_at_period_start:
@@ -337,8 +336,16 @@ class Subscription(Document):
return True
# Check invoice dates and make sure it doesn't have outstanding invoices
- return getdate(nowdate()) >= getdate(self.current_invoice_start) and not self.has_outstanding_invoice()
-
+ return getdate(nowdate()) >= getdate(self.current_invoice_start)
+
+ def is_current_invoice_generated(self):
+ invoice = self.get_current_invoice()
+
+ if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
+ return True
+
+ return False
+
def is_current_invoice_paid(self):
if self.is_new_subscription():
return False
@@ -346,7 +353,7 @@ class Subscription(Document):
last_invoice = frappe.get_doc('Sales Invoice', self.invoices[-1].invoice)
if getdate(last_invoice.posting_date) == getdate(self.current_invoice_start) and last_invoice.status == 'Paid':
return True
-
+
return False
def process_for_active(self):
@@ -358,7 +365,8 @@ class Subscription(Document):
2. Change the `Subscription` status to 'Past Due Date'
3. Change the `Subscription` status to 'Cancelled'
"""
- if not self.is_current_invoice_paid() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
+ if not self.is_current_invoice_generated() and not self.is_current_invoice_paid() and \
+ (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
self.generate_invoice()
if self.current_invoice_is_past_due():
self.status = 'Past Due Date'
@@ -369,6 +377,9 @@ class Subscription(Document):
if self.cancel_at_period_end and getdate(nowdate()) > getdate(self.current_invoice_end):
self.cancel_subscription_at_period_end()
+ if self.is_current_invoice_generated() and getdate() > getdate(self.current_invoice_end):
+ self.update_subscription_period(add_days(self.current_invoice_end, 1))
+
def cancel_subscription_at_period_end(self):
"""
Called when `Subscription.cancel_at_period_end` is truthy
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index 3d96f233b40..e38de252699 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -101,19 +101,19 @@ class TestSubscription(unittest.TestCase):
subscription.delete()
def test_invoice_is_generated_at_end_of_billing_period(self):
+ start_date = add_to_date(nowdate(), months=-1)
subscription = frappe.new_doc('Subscription')
subscription.customer = '_Test Customer'
- subscription.start = '2018-01-01'
+ subscription.start = start_date
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
subscription.insert()
self.assertEqual(subscription.status, 'Active')
- self.assertEqual(subscription.current_invoice_start, '2018-01-01')
- self.assertEqual(subscription.current_invoice_end, '2018-01-31')
+ self.assertEqual(subscription.current_invoice_start, start_date)
+ self.assertEqual(subscription.current_invoice_end, add_days(nowdate(), -1))
subscription.process()
self.assertEqual(len(subscription.invoices), 1)
- self.assertEqual(subscription.current_invoice_start, '2018-01-01')
self.assertEqual(subscription.status, 'Past Due Date')
subscription.delete()
@@ -137,7 +137,6 @@ class TestSubscription(unittest.TestCase):
subscription.process()
self.assertEqual(subscription.status, 'Active')
- self.assertEqual(subscription.current_invoice_start, add_months(subscription.start, 1))
self.assertEqual(len(subscription.invoices), 1)
subscription.delete()
@@ -538,3 +537,23 @@ class TestSubscription(unittest.TestCase):
settings.save()
subscription.delete()
+
+ def test_duplicate_invoice_check(self):
+ subscription = frappe.new_doc('Subscription')
+ subscription.customer = '_Test Customer'
+ subscription.generate_invoice_at_period_start = True
+ subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
+ subscription.start = nowdate()
+ subscription.save()
+
+ # Generate invoice for the current invoicing period
+ subscription.process()
+ subscription.load_from_db()
+ self.assertEqual(len(subscription.invoices), 1)
+
+ # Proccess subscription again for the same period
+ subscription.process()
+ subscription.load_from_db()
+
+ # No new invoice should be created for current period
+ self.assertEqual(len(subscription.invoices), 1)
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/page/pos/pos.js b/erpnext/accounts/page/pos/pos.js
index cecf7f5e6fc..1e82e54cdf8 100755
--- a/erpnext/accounts/page/pos/pos.js
+++ b/erpnext/accounts/page/pos/pos.js
@@ -1064,7 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
$(frappe.render_template("pos_item", {
item_code: escape(obj.name),
item_price: item_price,
- title: obj.name || obj.item_name,
+ title: obj.name === obj.item_name ? obj.name : obj.item_name,
item_name: obj.name === obj.item_name ? "" : obj.item_name,
item_image: obj.image,
item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj),
@@ -1546,7 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
$.each(this.frm.doc.items || [], function (i, d) {
$(frappe.render_template("pos_bill_item_new", {
item_code: escape(d.item_code),
- title: d.item_code || d.item_name,
+ title: d.item_code === d.item_name ? d.item_code : d.item_name,
item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("
" + d.item_name),
qty: d.qty,
discount_percentage: d.discount_percentage || 0.0,
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/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 58117b68c52..9e3f3b739c2 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -14,7 +14,7 @@ import frappe, erpnext
from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency
from erpnext.accounts.utils import get_fiscal_year
from frappe import _
-from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr)
+from frappe.utils import (flt, getdate, get_first_day, add_months, add_days, formatdate, cstr, cint)
from six import itervalues
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children
@@ -43,7 +43,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, periodicity, accumulated_v
start_date = year_start_date
months = get_months(year_start_date, year_end_date)
- for i in range(math.ceil(months / months_to_add)):
+ for i in range(cint(math.ceil(months / months_to_add))):
period = frappe._dict({
"from_date": start_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 bfb44e5c30a..719b8de92be 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, get_datetime
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")
@@ -149,6 +154,10 @@ class Asset(AccountsController):
def make_asset_movement(self):
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
reference_docname = self.purchase_receipt or self.purchase_invoice
+ transaction_date = getdate(self.purchase_date)
+ if reference_docname:
+ posting_date, posting_time = frappe.db.get_value(reference_doctype, reference_docname, ["posting_date", "posting_time"])
+ transaction_date = get_datetime("{} {}".format(posting_date, posting_time))
assets = [{
'asset': self.name,
'asset_name': self.asset_name,
@@ -160,7 +169,7 @@ class Asset(AccountsController):
'assets': assets,
'purpose': 'Receipt',
'company': self.company,
- 'transaction_date': getdate(nowdate()),
+ 'transaction_date': transaction_date,
'reference_doctype': reference_doctype,
'reference_name': reference_docname
}).insert()
@@ -308,7 +317,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/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 5dacfb0e02e..ee07892fe43 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -1055,7 +1055,8 @@
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
- "modified": "2020-07-01 12:40:45.240948",
+ "links": [],
+ "modified": "2020-09-14 14:36:12.418690",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
@@ -1112,5 +1113,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "supplier",
- "title_field": "title"
+ "title_field": "supplier",
+ "track_changes": 1
}
\ No newline at end of file
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/buying/doctype/supplier_scorecard/supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard.py
index af109ba2848..e956afdf749 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 737ddd6ddd5..35a78cb98fc 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 4f34f7f3ce4..bc90011bf12 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -7,6 +7,7 @@ import json
from frappe import _, throw
from frappe.utils import (today, flt, cint, fmt_money, formatdate,
getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form)
+from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied, WorkflowPermissionError
from erpnext.stock.get_item_details import get_conversion_factor, get_item_details
from erpnext.setup.utils import get_exchange_rate
from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency
@@ -19,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")
@@ -1126,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
@@ -1139,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.")
@@ -1161,9 +1175,10 @@ 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 check_and_delete_children(parent, data):
+def validate_and_delete_children(parent, data):
deleted_children = []
updated_item_names = [d.get("docname") for d in data]
for item in parent.items:
@@ -1190,18 +1205,40 @@ def check_and_delete_children(parent, data):
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
- def check_permissions(doc, perm_type='create'):
+ def check_doc_permissions(doc, perm_type='create'):
try:
doc.check_permission(perm_type)
- except:
- action = "add" if perm_type == 'create' else "update"
- frappe.throw(_("You do not have permissions to {} items in a Sales Order.").format(action), title=_("Insufficient Permissions"))
+ except frappe.PermissionError:
+ actions = { 'create': 'add', 'write': 'update', 'cancel': 'remove' }
+
+ 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:
+ return
+
+ workflow_doc = frappe.get_doc("Workflow", workflow)
+ current_state = doc.get(workflow_doc.workflow_state_field)
+ roles = frappe.get_roles()
+
+ transitions = []
+ for transition in workflow_doc.transitions:
+ if transition.next_state == current_state and transition.allowed in roles:
+ if not is_transition_condition_satisfied(transition, doc):
+ continue
+ transitions.append(transition.as_dict())
+
+ if not transitions:
+ 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):
- if parent_doctype == "Sales Order":
- return set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row)
- if parent_doctype == "Purchase Order":
- return set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, item_row)
+ new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
+ return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row)
def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
@@ -1215,16 +1252,17 @@ 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_and_delete_children(parent, data)
+ check_doc_permissions(parent, 'cancel')
+ validate_and_delete_children(parent, data)
for d in data:
new_child_flag = False
if not d.get("docname"):
new_child_flag = True
- check_permissions(parent, 'create')
+ check_doc_permissions(parent, 'create')
child_item = get_new_child_item(d)
else:
- check_permissions(parent, 'write')
+ check_doc_permissions(parent, 'write')
child_item = frappe.get_doc(parent_doctype + ' Item', d.get("docname"))
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
@@ -1331,6 +1369,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_prevdoc_status('submit')
parent.update_delivery_status()
+ parent.reload()
+ validate_workflow_conditions(parent)
+
parent.update_blanket_order()
parent.update_billing_percentage()
parent.set_status()
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index aef7bcd156b..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,8 +321,10 @@ 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)
+ qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty:
qty = batch_data['qty']
raw_material.batch_no = batch_data['batch']
@@ -334,6 +336,10 @@ class BuyingController(StockController):
rm = self.append('supplied_items', {})
rm.update(raw_material_data)
+ 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
@@ -844,7 +850,7 @@ def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
AND IFNULL(sed.t_warehouse, '') != ''
- AND sed.subcontracted_item = %s
+ AND IFNULL(sed.subcontracted_item, '') in ('', %s)
GROUP BY sed.item_code, sed.subcontracted_item
"""
raw_materials = frappe.db.multisql({
@@ -861,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"],
@@ -975,14 +991,15 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
SELECT
sed.batch_no,
SUM(sed.qty) AS qty,
- sed.item_code
+ sed.item_code,
+ sed.subcontracted_item
FROM `tabStock Entry` se,`tabStock Entry Detail` sed
WHERE
se.name = sed.parent
AND se.docstatus=1
AND se.purpose='Send to Subcontractor'
AND se.purchase_order = %s
- AND sed.subcontracted_item = %s
+ AND ifnull(sed.subcontracted_item, '') in ('', %s)
AND sed.batch_no IS NOT NULL
GROUP BY
sed.batch_no,
@@ -990,8 +1007,10 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
""", (purchase_order, fg_item), as_dict=1)
for batch_data in transferred_batches:
- transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
- transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
+ key = ((batch_data.item_code, fg_item)
+ if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
+ transferred_batch_qty_map.setdefault(key, {})
+ transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map
@@ -1028,10 +1047,11 @@ 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):
+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), {})
- backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {})
+ if not transferred_batches:
+ transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
available_batches = []
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index b465a106f0e..da99f1267f6 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -249,7 +249,7 @@ class StatusUpdater(Document):
args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s)
from `tab%(second_source_dt)s`
where `%(second_join_field)s`="%(detail_id)s"
- and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s), 0) """ % args
+ and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0) """ % args
if args['detail_id']:
if not args.get("extra_cond"): args["extra_cond"] = ""
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2f275bb3c91..8cad82c3e25 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -242,10 +242,11 @@ class StockController(AccountsController):
_(self.doctype), self.name, item.get("item_code")))
def delete_auto_created_batches(self):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for d in self.items:
if not d.batch_no: continue
- serial_nos = [sr.name for sr in frappe.get_all("Serial No", {'batch_no': d.batch_no})]
+ serial_nos = get_serial_nos(d.serial_no)
if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
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/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index f23b4220849..73056f16ed1 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -82,7 +82,8 @@ def add_attendance(events, start, end, conditions=None):
e = {
"name": d.name,
"doctype": "Attendance",
- "date": d.attendance_date,
+ "start": d.attendance_date,
+ "end": d.attendance_date,
"title": cstr(d.status),
"docstatus": d.docstatus
}
diff --git a/erpnext/hr/doctype/attendance/attendance_calendar.js b/erpnext/hr/doctype/attendance/attendance_calendar.js
index 104f09d69ff..45664896965 100644
--- a/erpnext/hr/doctype/attendance/attendance_calendar.js
+++ b/erpnext/hr/doctype/attendance/attendance_calendar.js
@@ -1,12 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.views.calendar["Attendance"] = {
- field_map: {
- "start": "attendance_date",
- "end": "attendance_date",
- "id": "name",
- "docstatus": 1
- },
options: {
header: {
left: 'prev,next today',
diff --git a/erpnext/hr/doctype/leave_application/leave_application.json b/erpnext/hr/doctype/leave_application/leave_application.json
index 9cc9c87f7f8..ee85a8437ee 100644
--- a/erpnext/hr/doctype/leave_application/leave_application.json
+++ b/erpnext/hr/doctype/leave_application/leave_application.json
@@ -166,7 +166,6 @@
"fieldtype": "Column Break"
},
{
- "allow_on_submit": 1,
"default": "Open",
"fieldname": "status",
"fieldtype": "Select",
@@ -245,9 +244,10 @@
],
"icon": "fa fa-calendar",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"max_attachments": 3,
- "modified": "2020-08-13 17:22:44.832397",
+ "modified": "2020-09-23 19:11:58.806837",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Application",
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 1622fb38eec..cac4f33a237 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -56,7 +56,7 @@ class LeaveApplication(Document):
def on_cancel(self):
self.create_leave_ledger_entry(submit=False)
- self.status = "Cancelled"
+ self.db_set("status", "Cancelled")
# notify leave applier about cancellation
self.notify_employee()
self.cancel_attendance()
@@ -433,6 +433,8 @@ def get_leave_details(employee, date):
'from_date': ('<=', date),
'to_date': ('>=', date),
'leave_type': allocation.leave_type,
+ 'employee': employee,
+ 'docstatus': 1
}, 'SUM(total_leaves_allocated)') or 0
remaining_leaves = get_leave_balance_on(employee, d, date, to_date = allocation.to_date,
@@ -444,7 +446,7 @@ def get_leave_details(employee, date):
leave_allocation[d] = {
"total_leaves": total_allocated_leaves,
- "expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
+ "expired_leaves": max(total_allocated_leaves - (remaining_leaves + leaves_taken), 0),
"leaves_taken": leaves_taken,
"pending_leaves": leaves_pending,
"remaining_leaves": remaining_leaves}
@@ -597,7 +599,7 @@ def get_leave_entries(employee, leave_type, from_date, to_date):
is_carry_forward, is_expired
FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
- AND docstatus=1
+ AND docstatus=1
AND (leaves<0
OR is_expired=1)
AND (from_date between %(from_date)s AND %(to_date)s
@@ -790,4 +792,4 @@ def get_leave_approver(employee):
leave_approver = frappe.db.get_value('Department Approver', {'parent': department,
'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
- return leave_approver
\ No newline at end of file
+ return leave_approver
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index 2b168d1d76d..cb8d3a02068 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -159,6 +159,7 @@ frappe.ui.form.on('Production Plan', {
get_sales_orders: function(frm) {
frappe.call({
method: "get_open_sales_orders",
+ freeze: true,
doc: frm.doc,
callback: function(r) {
refresh_field("sales_orders");
@@ -169,6 +170,7 @@ frappe.ui.form.on('Production Plan', {
get_material_request: function(frm) {
frappe.call({
method: "get_pending_material_requests",
+ freeze: true,
doc: frm.doc,
callback: function() {
refresh_field('material_requests');
@@ -188,7 +190,7 @@ frappe.ui.form.on('Production Plan', {
},
get_items_for_mr: function(frm) {
- const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom',
+ const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom',
'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type'];
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
@@ -219,7 +221,7 @@ frappe.ui.form.on('Production Plan', {
download_materials_required: function(frm) {
let get_template_url = 'erpnext.manufacturing.doctype.production_plan.production_plan.download_raw_materials';
- open_url_post(frappe.request.url, { cmd: get_template_url, production_plan: frm.doc.name });
+ open_url_post(frappe.request.url, { cmd: get_template_url, doc: frm.doc });
},
show_progress: function(frm) {
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 8ff691cc300..aa80dcfed24 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -322,12 +322,13 @@ class ProductionPlan(Document):
work_orders = []
bom_data = {}
- get_sub_assembly_items(item.get("bom_no"), bom_data)
+ get_sub_assembly_items(item.get("bom_no"), bom_data, item.get("qty"))
for key, data in bom_data.items():
data.update({
- 'qty': data.get("stock_qty") * item.get("qty"),
+ 'qty': data.get("stock_qty"),
'production_plan': self.name,
+ 'use_multi_level_bom': item.get("use_multi_level_bom"),
'company': self.company,
'fg_warehouse': item.get("fg_warehouse"),
'update_consumed_material_cost_in_project': 0
@@ -421,14 +422,13 @@ class ProductionPlan(Document):
msgprint(_("No material request created"))
@frappe.whitelist()
-def download_raw_materials(production_plan):
- doc = frappe.get_doc('Production Plan', production_plan)
- doc.check_permission()
-
+def download_raw_materials(doc):
item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse',
'projected Qty', 'Actual Qty']]
- doc = doc.as_dict()
+ if isinstance(doc, string_types):
+ doc = frappe._dict(json.loads(doc))
+
for d in get_items_for_material_requests(doc, ignore_existing_ordered_qty=True):
item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('quantity'),
d.get('warehouse'), d.get('projected_qty'), d.get('actual_qty')])
@@ -724,7 +724,7 @@ def get_item_data(item_code):
# "description": item_details.get("description")
}
-def get_sub_assembly_items(bom_no, bom_data):
+def get_sub_assembly_items(bom_no, bom_data, to_produce_qty):
data = get_children('BOM', parent = bom_no)
for d in data:
if d.expandable:
@@ -741,6 +741,6 @@ def get_sub_assembly_items(bom_no, bom_data):
})
bom_item = bom_data.get(key)
- bom_item["stock_qty"] += d.stock_qty / d.parent_bom_qty
+ bom_item["stock_qty"] += (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
- get_sub_assembly_items(bom_item.get("bom_no"), bom_data)
+ get_sub_assembly_items(bom_item.get("bom_no"), bom_data, bom_item["stock_qty"])
diff --git a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
index 26f580db339..c67330ad45f 100644
--- a/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/test_production_plan.py
@@ -158,6 +158,46 @@ class TestProductionPlan(unittest.TestCase):
self.assertTrue(mr.material_request_type, 'Customer Provided')
self.assertTrue(mr.customer, '_Test Customer')
+ def test_production_plan_with_multi_level_bom(self):
+ #|Item Code | Qty |
+ #|Test BOM 1 | 1 |
+ #| Test BOM 2 | 2 |
+ #| Test BOM 3 | 3 |
+
+ for item_code in ["Test BOM 1", "Test BOM 2", "Test BOM 3", "Test RM BOM 1"]:
+ create_item(item_code, is_stock_item=1)
+
+ # created bom upto 3 level
+ if not frappe.db.get_value('BOM', {'item': "Test BOM 3"}):
+ make_bom(item = "Test BOM 3", raw_materials = ["Test RM BOM 1"], rm_qty=3)
+
+ if not frappe.db.get_value('BOM', {'item': "Test BOM 2"}):
+ make_bom(item = "Test BOM 2", raw_materials = ["Test BOM 3"], rm_qty=3)
+
+ if not frappe.db.get_value('BOM', {'item': "Test BOM 1"}):
+ make_bom(item = "Test BOM 1", raw_materials = ["Test BOM 2"], rm_qty=2)
+
+ item_code = "Test BOM 1"
+ pln = frappe.new_doc('Production Plan')
+ pln.company = "_Test Company"
+ pln.append("po_items", {
+ "item_code": item_code,
+ "bom_no": frappe.db.get_value('BOM', {'item': "Test BOM 1"}),
+ "planned_qty": 3,
+ "make_work_order_for_sub_assembly_items": 1
+ })
+
+ pln.submit()
+ pln.make_work_order()
+
+ #last level sub-assembly work order produce qty
+ to_produce_qty = frappe.db.get_value("Work Order",
+ {"production_plan": pln.name, "production_item": "Test BOM 3"}, "qty")
+
+ self.assertEqual(to_produce_qty, 18.0)
+ pln.cancel()
+ frappe.delete_doc("Production Plan", pln.name)
+
def create_production_plan(**args):
args = frappe._dict(args)
@@ -205,7 +245,7 @@ def make_bom(**args):
bom.append('items', {
'item_code': item,
- 'qty': 1,
+ 'qty': args.rm_qty or 1.0,
'uom': item_doc.stock_uom,
'stock_uom': item_doc.stock_uom,
'rate': item_doc.valuation_rate or args.rate,
@@ -213,4 +253,4 @@ def make_bom(**args):
bom.insert(ignore_permissions=True)
bom.submit()
- return bom
\ No newline at end of file
+ return bom
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 831541ccb1f..afb6db35f27 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -677,3 +677,4 @@ erpnext.patches.v12_0.set_multi_uom_in_rfq
erpnext.patches.v12_0.update_state_code_for_daman_and_diu
erpnext.patches.v12_0.rename_lost_reason_detail
erpnext.patches.v12_0.update_leave_application_status
+erpnext.patches.v12_0.update_payment_entry_status
\ No newline at end of file
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/patches/v12_0/update_payment_entry_status.py b/erpnext/patches/v12_0/update_payment_entry_status.py
new file mode 100644
index 00000000000..8d033800af3
--- /dev/null
+++ b/erpnext/patches/v12_0/update_payment_entry_status.py
@@ -0,0 +1,7 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "payment_entry")
+
+ frappe.db.sql(""" UPDATE `tabPayment Entry` set status = 'Cancelled' WHERE docstatus = 2 """)
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js
index f4eaad58dae..6e97d811fc1 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/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/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
index 59389ce3269..4060a553bd8 100644
--- a/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
+++ b/erpnext/regional/report/hsn_wise_summary_of_outward_supplies/hsn_wise_summary_of_outward_supplies.py
@@ -43,6 +43,12 @@ def _execute(filters=None):
data.append(row)
added_item.append((d.parent, d.item_code))
+ # gst is already added, just add qty and taxable value
+ else:
+ row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.base_net_amount, d.base_net_amount]
+ for tax in tax_columns:
+ row += [0]
+ data.append(row)
if data:
data = get_merged_data(columns, data) # merge same hsn code data
return columns, data
diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py
index 67834d12210..ceea8460bea 100644
--- a/erpnext/regional/report/irs_1099/irs_1099.py
+++ b/erpnext/regional/report/irs_1099/irs_1099.py
@@ -19,6 +19,11 @@ def execute(filters=None):
if not filters:
filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0])
filters.setdefault('company', frappe.db.get_default("company"))
+
+ region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company })
+ if region != 'United States':
+ return [],[]
+
data = []
columns = get_columns()
data = frappe.db.sql("""
diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py
index 688f14576c8..ad95010a9ac 100644
--- a/erpnext/regional/united_states/test_united_states.py
+++ b/erpnext/regional/united_states/test_united_states.py
@@ -24,7 +24,7 @@ class TestUnitedStates(unittest.TestCase):
def test_irs_1099_report(self):
make_payment_entry_to_irs_1099_supplier()
- filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company"})
+ filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"})
columns, data = execute_1099_report(filters)
print(columns, data)
expected_row = {'supplier': '_US 1099 Test Supplier',
@@ -42,10 +42,10 @@ def make_payment_entry_to_irs_1099_supplier():
pe = frappe.new_doc("Payment Entry")
pe.payment_type = "Pay"
- pe.company = "_Test Company"
+ pe.company = "_Test Company 1"
pe.posting_date = "2016-01-10"
- pe.paid_from = "_Test Bank USD - _TC"
- pe.paid_to = "_Test Payable USD - _TC"
+ pe.paid_from = "_Test Bank USD - _TC1"
+ pe.paid_to = "_Test Payable USD - _TC1"
pe.paid_amount = 100
pe.received_amount = 100
pe.reference_no = "For IRS 1099 testing"
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index f88289871e9..ed3a96446a0 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import json
import frappe.utils
-from frappe.utils import cstr, flt, getdate, cint, nowdate, add_days, get_link_to_form
+from frappe.utils import cstr, flt, getdate, cint, nowdate, add_days, get_link_to_form, strip_html
from frappe import _
from six import string_types
from frappe.model.utils import get_fetch_values
@@ -994,15 +994,20 @@ def make_raw_material_request(items, company, sales_order, project=None):
))
for item in raw_materials:
item_doc = frappe.get_cached_doc('Item', item.get('item_code'))
+
schedule_date = add_days(nowdate(), cint(item_doc.lead_time_days))
- material_request.append('items', {
- 'item_code': item.get('item_code'),
- 'qty': item.get('quantity'),
- 'schedule_date': schedule_date,
- 'warehouse': item.get('warehouse'),
- 'sales_order': sales_order,
- 'project': project
+ row = material_request.append('items', {
+ 'item_code': item.get('item_code'),
+ 'qty': item.get('quantity'),
+ 'schedule_date': schedule_date,
+ 'warehouse': item.get('warehouse'),
+ 'sales_order': sales_order,
+ 'project': project
})
+
+ if not (strip_html(item.get("description")) and strip_html(item_doc.description)):
+ row.description = item_doc.item_name or item.get('item_code')
+
material_request.insert()
material_request.flags.ignore_permissions = 1
material_request.run_method("set_missing_values")
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 5e360902e83..31c328fdf3e 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -416,8 +416,44 @@ class TestSalesOrder(unittest.TestCase):
# add new item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}])
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')
+
+ user = 'test@example.com'
+ test_user = frappe.get_doc('User', user)
+ test_user.add_roles("Sales User", "Test Junior Approver")
+ frappe.set_user(user)
+
+ # user shouldn't be able to edit since grand_total will become > 200 if qty is doubled
+ trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 150, 'qty' : 2, 'docname': so.items[0].name}])
+ self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Sales Order', trans_item, so.name)
+
+ frappe.set_user("Administrator")
+ user2 = 'test2@example.com'
+ test_user2 = frappe.get_doc('User', user2)
+ test_user2.add_roles("Sales User", "Test Approver")
+ frappe.set_user(user2)
+
+ # Test Approver is allowed to edit with grand_total > 200
+ update_child_qty_rate("Sales Order", trans_item, so.name)
+ so.reload()
+ self.assertEqual(so.items[0].qty, 2)
+
+ frappe.set_user("Administrator")
+ test_user.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
+ test_user2.remove_roles("Sales User", "Test Junior Approver", "Test Approver")
+ workflow.is_active = 0
+ workflow.save()
+
def test_update_child_qty_rate_product_bundle(self):
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Product Bundle Item"):
@@ -953,3 +989,37 @@ def get_reserved_qty(item_code="_Test Item", warehouse="_Test Warehouse - _TC"):
"reserved_qty"))
test_dependencies = ["Currency Exchange"]
+
+def make_sales_order_workflow():
+ if frappe.db.exists('Workflow', 'SO Test Workflow'):
+ doc = frappe.get_doc("Workflow", "SO Test Workflow")
+ doc.set("is_active", 1)
+ doc.save()
+ return doc
+
+ frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True)
+ frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True)
+ frappe.db.commit()
+ frappe.cache().hdel('roles', frappe.session.user)
+
+ workflow = frappe.get_doc({
+ "doctype": "Workflow",
+ "workflow_name": "SO Test Workflow",
+ "document_type": "Sales Order",
+ "workflow_state_field": "workflow_state",
+ "is_active": 1,
+ "send_email_alert": 0,
+ })
+ 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,
+ condition = 'doc.grand_total < 200'
+ ))
+ workflow.append('transitions', dict(
+ state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Approver', allow_self_approval = 1,
+ condition = 'doc.grand_total > 200'
+ ))
+ workflow.insert(ignore_permissions=True)
+
+ return workflow
\ No newline at end of file
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/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 62aebbaf504..6be20a2e71d 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -121,12 +121,18 @@ erpnext.stock.DeliveryNoteController = erpnext.selling.SellingController.extend(
if (this.frm.doc.docstatus===0) {
this.frm.add_custom_button(__('Sales Order'),
function() {
+ if (!me.frm.doc.customer) {
+ frappe.throw({
+ title: __("Mandatory"),
+ message: __("Please Select a Customer")
+ });
+ }
erpnext.utils.map_current_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
source_doctype: "Sales Order",
target: me.frm,
setters: {
- customer: me.frm.doc.customer || undefined,
+ customer: me.frm.doc.customer,
},
get_query_filters: {
docstatus: 1,
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index f045e4f9114..61d90392364 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -13,6 +13,7 @@ from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_ord
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry
import unittest
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
+from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError
class TestItemAlternative(unittest.TestCase):
def setUp(self):
@@ -110,8 +111,11 @@ def make_items():
if not frappe.db.exists('Item', item_code):
create_item(item_code)
- create_stock_reconciliation(item_code="Test FG A RW 1",
- warehouse='_Test Warehouse - _TC', qty=10, rate=2000)
+ try:
+ create_stock_reconciliation(item_code="Test FG A RW 1",
+ warehouse='_Test Warehouse - _TC', qty=10, rate=2000)
+ except EmptyStockReconciliationItemsError:
+ pass
if frappe.db.exists('Item', 'Test FG A RW 1'):
doc = frappe.get_doc('Item', 'Test FG A RW 1')
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index 89ca1bef856..27946586eaa 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -101,12 +101,18 @@ erpnext.stock.PurchaseReceiptController = erpnext.buying.BuyingController.extend
if (this.frm.doc.docstatus == 0) {
this.frm.add_custom_button(__('Purchase Order'),
function () {
+ if (!me.frm.doc.supplier) {
+ frappe.throw({
+ title: __("Mandatory"),
+ message: __("Please Select a Supplier")
+ });
+ }
erpnext.utils.map_current_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
source_doctype: "Purchase Order",
target: me.frm,
setters: {
- supplier: me.frm.doc.supplier || undefined,
+ supplier: me.frm.doc.supplier,
},
get_query_filters: {
docstatus: 1,
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index be2453373e4..42cc473b18d 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -207,6 +207,7 @@ class PurchaseReceipt(BuyingController):
from erpnext.accounts.general_ledger import process_gl_map
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
+ cogs_account = self.get_company_default("default_expense_account")
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
@@ -288,7 +289,7 @@ class PurchaseReceipt(BuyingController):
if self.is_return or flt(d.item_tax_amount):
loss_account = expenses_included_in_valuation
else:
- loss_account = stock_rbnb
+ loss_account = cogs_account
gl_entries.append(self.get_gl_dict({
"account": loss_account,
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 8008b48850c..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()
@@ -579,6 +578,67 @@ class TestPurchaseReceipt(unittest.TestCase):
self.assertEquals(pi2.items[0].qty, 2)
self.assertEquals(pi2.items[1].qty, 1)
+ def test_subcontracted_pr_for_multi_transfer_batches(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry, make_purchase_receipt
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import (update_backflush_based_on,
+ create_purchase_order)
+
+ update_backflush_based_on("Material Transferred for Subcontract")
+ item_code = "_Test Subcontracted FG Item 3"
+
+ make_item('Sub Contracted Raw Material 3', {
+ 'is_stock_item': 1,
+ 'is_sub_contracted_item': 1,
+ 'has_batch_no': 1,
+ 'create_new_batch': 1
+ })
+
+ create_subcontracted_item(item_code=item_code, has_batch_no=1,
+ raw_materials=["Sub Contracted Raw Material 3"])
+
+ order_qty = 500
+ po = create_purchase_order(item_code=item_code, qty=order_qty,
+ is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
+
+ ste1=make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Sub Contracted Raw Material 3", qty=300, basic_rate=100)
+ ste2=make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Sub Contracted Raw Material 3", qty=200, basic_rate=100)
+
+ transferred_batch = {
+ ste1.items[0].batch_no : 300,
+ ste2.items[0].batch_no : 200
+ }
+
+ rm_items = [
+ {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
+ "qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
+ {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
+ "qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}
+ ]
+
+ rm_item_string = json.dumps(rm_items)
+ se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
+ self.assertEqual(len(se.items), 2)
+ se.items[0].batch_no = ste1.items[0].batch_no
+ se.items[1].batch_no = ste2.items[0].batch_no
+ se.submit()
+
+ supplied_qty = frappe.db.get_value("Purchase Order Item Supplied",
+ {"parent": po.name, "rm_item_code": "Sub Contracted Raw Material 3"}, "supplied_qty")
+
+ self.assertEqual(supplied_qty, 500.00)
+
+ pr = make_purchase_receipt(po.name)
+ pr.save()
+ self.assertEqual(len(pr.supplied_items), 2)
+
+ for row in pr.supplied_items:
+ self.assertEqual(transferred_batch.get(row.batch_no), row.consumed_qty)
+
+ update_backflush_based_on("BOM")
+
def get_gl_entries(voucher_type, voucher_no):
return frappe.db.sql("""select account, debit, credit, cost_center
from `tabGL Entry` where voucher_type=%s and voucher_no=%s
@@ -714,6 +774,33 @@ def make_purchase_receipt(**args):
pr.submit()
return pr
+def create_subcontracted_item(**args):
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ 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,
+ 'has_batch_no': args.get("has_batch_no") or 0
+ })
+
+ 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,
+ })
+
+ 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"))
test_dependencies = ["BOM", "Item Price", "Location"]
test_records = frappe.get_test_records('Purchase Receipt')
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index a9f3cd09ef5..b99e98b65c1 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -72,7 +72,8 @@
"fieldname": "reference_type",
"fieldtype": "Select",
"label": "Reference Type",
- "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry"
+ "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry",
+ "reqd": 1
},
{
"fieldname": "reference_name",
@@ -83,7 +84,8 @@
"label": "Reference Name",
"oldfieldname": "purchase_receipt_no",
"oldfieldtype": "Link",
- "options": "reference_type"
+ "options": "reference_type",
+ "reqd": 1
},
{
"fieldname": "section_break_7",
@@ -230,8 +232,10 @@
],
"icon": "fa fa-search",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-07-12 12:07:23.153698",
+ "links": [],
+ "modified": "2020-09-12 16:11:31.910508",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
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..fd921cef4e9 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 = [
@@ -139,6 +157,7 @@ frappe.ui.form.on('Stock Entry', {
mr_item.item_code = item.item_code;
mr_item.item_name = item.item_name;
mr_item.uom = item.uom;
+ mr_item.stock_uom = item.stock_uom;
mr_item.conversion_factor = item.conversion_factor;
mr_item.item_group = item.item_group;
mr_item.description = item.description;
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index 0174858dc36..961f5f45325 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -500,7 +500,7 @@ class StockEntry(StockController):
d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually:
d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
- d.basic_amount = d.basic_rate * d.qty
+ d.basic_amount = d.basic_rate * flt(d.qty)
def distribute_additional_costs(self):
if self.purpose == "Material Issue":
@@ -556,8 +556,9 @@ class StockEntry(StockController):
qty_allowance = flt(frappe.db.get_single_value("Buying Settings",
"over_transfer_allowance"))
- if (self.purpose == "Send to Subcontractor" and self.purchase_order and
- backflush_raw_materials_based_on == 'BOM'):
+ if not (self.purpose == "Send to Subcontractor" and self.purchase_order): return
+
+ if (backflush_raw_materials_based_on == 'BOM'):
purchase_order = frappe.get_doc("Purchase Order", self.purchase_order)
for se_item in self.items:
item_code = se_item.original_item or se_item.item_code
@@ -594,6 +595,11 @@ class StockEntry(StockController):
if flt(total_supplied, precision) > flt(total_allowed, precision):
frappe.throw(_("Row {0}# Item {1} cannot be transferred more than {2} against Purchase Order {3}")
.format(se_item.idx, se_item.item_code, total_allowed, self.purchase_order))
+ elif backflush_raw_materials_based_on == "Material Transferred for Subcontract":
+ for row in self.items:
+ if not row.subcontracted_item:
+ frappe.throw(_("Row {0}: Subcontracted Item is mandatory for the raw material {1}")
+ .format(row.idx, frappe.bold(row.item_code)))
def validate_bom(self):
for d in self.get('items'):
@@ -797,6 +803,13 @@ class StockEntry(StockController):
ret.get('has_batch_no') and not args.get('batch_no')):
args.batch_no = get_batch_no(args['item_code'], args['s_warehouse'], args['qty'])
+ if self.purpose == "Send to Subcontractor" and self.get("purchase_order") and args.get('item_code'):
+ subcontract_items = frappe.get_all("Purchase Order Item Supplied",
+ {"parent": self.purchase_order, "rm_item_code": args.get('item_code')}, "main_item_code")
+
+ if subcontract_items and len(subcontract_items) == 1:
+ ret["subcontracted_item"] = subcontract_items[0].main_item_code
+
return ret
def set_items_for_stock_in(self):
@@ -1237,9 +1250,15 @@ class StockEntry(StockController):
#Update Supplied Qty in PO Supplied Items
frappe.db.sql("""UPDATE `tabPurchase Order Item Supplied` pos
- SET pos.supplied_qty = (SELECT ifnull(sum(transfer_qty), 0) FROM `tabStock Entry Detail` sed
- WHERE pos.name = sed.po_detail and sed.docstatus = 1)
- WHERE pos.docstatus = 1 and pos.parent = %s""", self.purchase_order)
+ SET
+ pos.supplied_qty = IFNULL((SELECT ifnull(sum(transfer_qty), 0)
+ FROM
+ `tabStock Entry Detail` sed, `tabStock Entry` se
+ WHERE
+ (pos.name = sed.po_detail OR sed.subcontracted_item = pos.main_item_code)
+ AND sed.docstatus = 1 AND se.name = sed.parent and se.purchase_order = %(po)s
+ ), 0)
+ WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})
#Update reserved sub contracted quantity in bin based on Supplied Item Details and
for d in self.get("items"):
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 2afabe1480d..84f535912d4 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from six import iteritems
def get_sle(**args):
@@ -483,6 +484,100 @@ class TestStockEntry(unittest.TestCase):
serial_no = get_serial_nos(se.get("items")[0].serial_no)[0]
self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse"))
+ def test_serial_batch_item_stock_entry(self):
+ """
+ Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item
+ 2) Cancel same Stock Entry
+ Expected Result: 1) Batch is created with Reference in Serial No
+ 2) Batch is deleted and Serial No is Inactive
+ """
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+
+ item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
+ if not item:
+ item = create_item("Batched and Serialised Item")
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.has_serial_no = 1
+ item.batch_number_series = "B-BATCH-.##"
+ item.serial_no_series = "S-.####"
+ item.save()
+ else:
+ item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
+
+ se = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
+ batch_no = se.items[0].batch_no
+ serial_no = get_serial_nos(se.items[0].serial_no)[0]
+ batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
+
+ batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
+ self.assertEqual(batch_in_serial_no, batch_no)
+
+ self.assertEqual(batch_qty, 1)
+
+ se.cancel()
+
+ batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no")
+ self.assertEqual(batch_in_serial_no, None)
+
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive")
+ self.assertEqual(frappe.db.exists("Batch", batch_no), None)
+
+ def test_serial_batch_item_qty_deduction(self):
+ """
+ Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch
+ Expected Result: 1) Cancelling first Stock Entry (origin transaction of created batch)
+ should throw a LinkExistsError
+ 2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch
+ and in that transaction only, Inactive.
+ """
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+
+ item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
+ if not item:
+ item = create_item("Batched and Serialised Item")
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.has_serial_no = 1
+ item.batch_number_series = "B-BATCH-.##"
+ item.serial_no_series = "S-.####"
+ item.save()
+ else:
+ item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
+
+ se1 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100)
+ batch_no = se1.items[0].batch_no
+ serial_no1 = get_serial_nos(se1.items[0].serial_no)[0]
+
+ # Check Source (Origin) Document of Batch
+ self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name)
+
+ se2 = make_stock_entry(item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100,
+ batch_no=batch_no)
+ serial_no2 = get_serial_nos(se2.items[0].serial_no)[0]
+
+ batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
+ self.assertEqual(batch_qty, 2)
+ frappe.db.commit()
+
+ # Cancelling Origin Document of Batch
+ self.assertRaises(frappe.LinkExistsError, se1.cancel)
+ frappe.db.rollback()
+
+ se2.cancel()
+
+ # Check decrease in Batch Qty
+ batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code)
+ self.assertEqual(batch_qty, 1)
+
+ # Check if Serial No from Stock Entry 1 is intact
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active")
+
+ # Check if Serial No from Stock Entry 2 is Unlinked and Inactive
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive")
+
def test_warehouse_company_validation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company')
set_perpetual_inventory(0, company)
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 9992d10febe..9d397df8bcd 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -1,5 +1,4 @@
{
- "actions": [],
"autoname": "hash",
"creation": "2013-03-29 18:22:12",
"doctype": "DocType",
@@ -17,6 +16,7 @@
"item_group",
"col_break2",
"item_name",
+ "subcontracted_item",
"section_break_8",
"description",
"column_break_10",
@@ -57,7 +57,6 @@
"material_request",
"material_request_item",
"original_item",
- "subcontracted_item",
"reference_section",
"against_stock_entry",
"ste_detail",
@@ -415,6 +414,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:parent.purpose == 'Send to Subcontractor'",
"fieldname": "subcontracted_item",
"fieldtype": "Link",
"label": "Subcontracted Item",
@@ -497,15 +497,12 @@
"depends_on": "eval:parent.purpose===\"Repack\" && doc.t_warehouse",
"fieldname": "set_basic_rate_manually",
"fieldtype": "Check",
- "label": "Set Basic Rate Manually",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Set Basic Rate Manually"
}
],
"idx": 1,
"istable": 1,
- "links": [],
- "modified": "2020-06-08 12:57:03.172887",
+ "modified": "2020-09-04 12:12:35.668198",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 5002426fbdd..ca59e67a676 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -45,6 +45,7 @@ class StockReconciliation(StockController):
def on_cancel(self):
self.delete_and_repost_sle()
self.make_gl_entries_on_cancel()
+ self.delete_auto_created_batches()
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""
@@ -164,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))
@@ -234,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 []
@@ -260,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/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index e027a6f3808..a679c9415d0 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -170,7 +170,7 @@ class TestStockReconciliation(unittest.TestCase):
warehouse = "_Test Warehouse for Stock Reco2 - _TC"
sr = create_stock_reconciliation(item_code=item_code,
- warehouse = warehouse, qty=5, rate=200, do_not_submit=1)
+ warehouse = warehouse, qty=5, rate=200, do_not_save=1, do_not_submit=1)
sr.save(ignore_permissions=True)
sr.submit()
@@ -204,6 +204,110 @@ class TestStockReconciliation(unittest.TestCase):
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
+ def test_stock_reco_for_serial_and_batch_item(self):
+ set_perpetual_inventory()
+
+ item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
+ if not item:
+ item = create_item("Batched and Serialised Item")
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.has_serial_no = 1
+ item.batch_number_series = "B-BATCH-.##"
+ item.serial_no_series = "S-.####"
+ item.save()
+ else:
+ item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
+
+ warehouse = "_Test Warehouse for Stock Reco2 - _TC"
+
+ sr = create_stock_reconciliation(item_code=item.item_code,
+ warehouse = warehouse, qty=1, rate=100)
+
+ batch_no = sr.items[0].batch_no
+
+ serial_nos = get_serial_nos(sr.items[0].serial_no)
+ self.assertEqual(len(serial_nos), 1)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no)
+
+ sr.cancel()
+
+ self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive")
+ self.assertEqual(frappe.db.exists("Batch", batch_no), None)
+
+ if frappe.db.exists("Serial No", serial_nos[0]):
+ frappe.delete_doc("Serial No", serial_nos[0])
+
+ def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self):
+ """
+ Behaviour: 1) Create Stock Reconciliation, which will be the origin document
+ of a new batch having a serial no
+ 2) Create a Stock Entry that adds a serial no to the same batch following this
+ Stock Reconciliation
+ 3) Cancel Stock Reconciliation
+ 4) Cancel Stock Entry
+ Expected Result: 3) Cancelling the Stock Reco throws a LinkExistsError since
+ Stock Entry is dependent on the batch involved
+ 4) Serial No only in the Stock Entry is Inactive and Batch qty decreases
+ """
+ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+ from erpnext.stock.doctype.batch.batch import get_batch_qty
+
+ set_perpetual_inventory()
+
+ item = frappe.db.exists("Item", {'item_name': 'Batched and Serialised Item'})
+ if not item:
+ item = create_item("Batched and Serialised Item")
+ item.has_batch_no = 1
+ item.create_new_batch = 1
+ item.has_serial_no = 1
+ item.batch_number_series = "B-BATCH-.##"
+ item.serial_no_series = "S-.####"
+ item.save()
+ else:
+ item = frappe.get_doc("Item", {'item_name': 'Batched and Serialised Item'})
+
+ warehouse = "_Test Warehouse for Stock Reco2 - _TC"
+
+ stock_reco = create_stock_reconciliation(item_code=item.item_code,
+ warehouse = warehouse, qty=1, rate=100)
+ batch_no = stock_reco.items[0].batch_no
+ serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0]
+
+ stock_entry = make_stock_entry(item_code=item.item_code, target=warehouse, qty=1, basic_rate=100,
+ batch_no=batch_no)
+ serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0]
+
+ # Check Batch qty after 2 transactions
+ batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
+ self.assertEqual(batch_qty, 2)
+ frappe.db.commit()
+
+ # Cancelling Origin Document of Batch
+ self.assertRaises(frappe.LinkExistsError, stock_reco.cancel)
+ frappe.db.rollback()
+
+ stock_entry.cancel()
+
+ # Check Batch qty after cancellation
+ batch_qty = get_batch_qty(batch_no, warehouse, item.item_code)
+ self.assertEqual(batch_qty, 1)
+
+ # Check if Serial No from Stock Reconcilation is intact
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no, "batch_no"), batch_no)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Active")
+
+ # Check if Serial No from Stock Entry is Unlinked and Inactive
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None)
+ self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive")
+
+ stock_reco.load_from_db()
+ stock_reco.cancel()
+
+ for sn in (serial_no, serial_no_2):
+ if frappe.db.exists("Serial No", sn):
+ frappe.delete_doc("Serial No", sn)
+
def test_stock_reco_for_same_item_with_multiple_batches(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -322,14 +426,14 @@ def create_stock_reconciliation(**args):
"batch_no": batch
})
- try:
- if args.do_not_save:
- return sr
+ if not args.do_not_save:
+ sr.insert()
+ try:
+ if not args.do_not_submit:
+ sr.submit()
+ except EmptyStockReconciliationItemsError:
+ pass
- if not args.do_not_submit:
- sr.submit()
- except EmptyStockReconciliationItemsError:
- pass
return sr
def set_valuation_method(item_code, valuation_method):
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 b23c908e07a..84e95e27ca0 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
}
]
}
\ No newline at end of file
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 7f7835f74ee..ec2ef35bb41 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
@@ -9,6 +9,9 @@ from frappe.utils import flt, cint, 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/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