Merge pull request #25798 from GangaManoj/asset-repair-refactor
refactor: Asset Repair
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 2f86c6c..335e8a1 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -301,17 +301,21 @@
start_date = add_months(today(), -1)
end_date = add_days(today(), -1)
- for record_type in ('Income', 'Expense'):
- doc = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- posting_date=posting_date,
- start_date=start_date,
- end_date=end_date,
- type=record_type
- ))
+ companies = frappe.get_all('Company')
- doc.insert()
- doc.submit()
+ for company in companies:
+ for record_type in ('Income', 'Expense'):
+ doc = frappe.get_doc(dict(
+ doctype='Process Deferred Accounting',
+ company=company.name,
+ posting_date=posting_date,
+ start_date=start_date,
+ end_date=end_date,
+ type=record_type
+ ))
+
+ doc.insert()
+ doc.submit()
def make_gl_entries(doc, credit_account, debit_account, against,
amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index 5f110e2..ffc9d1c 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -51,7 +51,7 @@
self.import_file, self.google_sheets_url
)
- if 'Bank Account' not in json.dumps(preview):
+ if 'Bank Account' not in json.dumps(preview['columns']):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index ec93314..189260a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -231,25 +231,25 @@
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_purchase_invoice_with_exchange_rate_difference(self):
- pr = make_purchase_receipt(currency = "USD", conversion_rate = 70)
- pi = make_purchase_invoice(currency = "USD", conversion_rate = 80, do_not_save = "True")
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as create_purchase_invoice
- pi.items[0].purchase_receipt = pr.name
- pi.items[0].pr_detail = pr.items[0].name
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
+ currency = "USD", conversion_rate = 70)
+
+ pi = create_purchase_invoice(pr.name)
+ pi.conversion_rate = 80
pi.insert()
pi.submit()
- # fetching the latest GL Entry with 'Exchange Gain/Loss - _TC' account
- gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - _TC'})
- voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
+ # Get exchnage gain and loss account
+ exchange_gain_loss_account = frappe.db.get_value('Company', pi.company, 'exchange_gain_loss_account')
- self.assertEqual(pi.name, voucher_no)
-
- exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
+ # fetching the latest GL Entry with exchange gain and loss account account
+ amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pi.name}, 'debit')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
- self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
+ self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
@@ -1031,21 +1031,21 @@
# Check GLE for Purchase Invoice
# Zero net effect on final TDS Payable on invoice
expected_gle = [
- ['_Test Account Cost for Goods Sold - _TC', 30000, 0],
- ['_Test Account Excise Duty - _TC', 0, 3000],
- ['Creditors - _TC', 0, 27000],
- ['TDS Payable - _TC', 3000, 3000]
+ ['_Test Account Cost for Goods Sold - _TC', 30000],
+ ['_Test Account Excise Duty - _TC', -3000],
+ ['Creditors - _TC', -27000],
+ ['TDS Payable - _TC', 0]
]
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s
+ group by account
order by account asc""", (purchase_invoice.name), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
- self.assertEqual(expected_gle[i][1], gle.debit)
- self.assertEqual(expected_gle[i][2], gle.credit)
+ self.assertEqual(expected_gle[i][1], gle.amount)
def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e025fc6..b97dc40 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -542,6 +542,7 @@
select company, sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where party_type = %s and party=%s
+ and is_cancelled = 0
group by company""", (party_type, party)))
for d in companies:
diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
index 9c9ada8..f1b231b 100644
--- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
+++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py
@@ -397,6 +397,7 @@
{'name': 'Budget', 'chartType': 'bar', 'values': budget_values},
{'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values}
]
- }
+ },
+ 'type' : 'bar'
}
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 7793af7..56a67bb 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -380,7 +380,7 @@
gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company,
gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency,
acc.account_name, acc.account_number
- from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s
+ from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0
{additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s
order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions),
{
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
index 8517dde..016b8ec 100644
--- a/erpnext/crm/doctype/appointment/appointment.json
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -102,7 +102,7 @@
}
],
"links": [],
- "modified": "2020-01-28 16:16:45.447213",
+ "modified": "2021-06-30 12:09:14.228756",
"modified_by": "Administrator",
"module": "CRM",
"name": "Appointment",
@@ -153,6 +153,18 @@
"role": "Sales User",
"share": 1,
"write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Employee",
+ "share": 1,
+ "write": 1
}
],
"quick_entry": 1,
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index d1d0968..ce3de40 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -168,12 +168,13 @@
if self.phone:
contact.append("phone_nos", {
"phone": self.phone,
- "is_primary": 1
+ "is_primary_phone": 1
})
if self.mobile_no:
contact.append("phone_nos", {
- "phone": self.mobile_no
+ "phone": self.mobile_no,
+ "is_primary_mobile_no":1
})
contact.insert(ignore_permissions=True)
diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py
index 9db8a4a..3070e6a 100644
--- a/erpnext/education/utils.py
+++ b/erpnext/education/utils.py
@@ -355,11 +355,11 @@
student = get_current_student()
course_enrollment = get_enrollment("course", course, student.name)
if not course_enrollment:
- program_enrollment = get_enrollment('program', program, student.name)
+ program_enrollment = get_enrollment('program', program.name, student.name)
if not program_enrollment:
frappe.throw(_("You are not enrolled in program {0}").format(program))
return
- return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program, student.name))
+ return student.enroll_in_course(course_name=course, program_enrollment=get_enrollment('program', program.name, student.name))
else:
return frappe.get_doc('Course Enrollment', course_enrollment)
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index b7d34b1..064dfb2 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -20,11 +20,10 @@
frappe.set_route("List", "Training Feedback");
});
}
- }
-});
+ frm.events.set_employee_query(frm);
+ },
-frappe.ui.form.on("Training Event Employee", {
- employee: function (frm) {
+ set_employee_query: function(frm) {
let emp = [];
for (let d in frm.doc.employees) {
if (frm.doc.employees[d].employee) {
@@ -40,3 +39,10 @@
});
}
});
+
+frappe.ui.form.on("Training Event Employee", {
+ employee: function(frm) {
+ frm.events.set_employee_query(frm);
+ }
+});
+
diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
index 2d313e9..bcb7d5e 100644
--- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json
+++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
@@ -19,6 +19,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
+ "no_copy": 1,
"options": "Employee"
},
{
@@ -68,7 +69,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-05-21 12:41:59.336237",
+ "modified": "2021-07-02 17:20:27.630176",
"modified_by": "Administrator",
"module": "HR",
"name": "Training Event Employee",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index c58f017..3bd1fe6 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1100,6 +1100,8 @@
},
'BOM Item': {
'doctype': 'BOM Item',
+ # stop get_mapped_doc copying parent bom_no to children
+ 'field_no_map': ['bom_no'],
'condition': lambda doc: doc.has_variants == 0
},
}, target_doc, postprocess)
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 57a5458..c89f7d6 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -8,6 +8,7 @@
from frappe.utils import cstr, flt
from frappe.test_runner import make_test_records
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
+from erpnext.manufacturing.doctype.bom.bom import make_variant_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -248,6 +249,37 @@
for reqd_item, created_item in zip(reqd_order, created_order):
self.assertEqual(reqd_item, created_item.item_code)
+ def test_generated_variant_bom(self):
+ from erpnext.controllers.item_variant import create_variant
+
+ template_item = make_item(
+ "_TestTemplateItem", {"has_variants": 1, "attributes": [{"attribute": "Test Size"},]}
+ )
+ variant = create_variant(template_item.item_code, {"Test Size": "Large"})
+ variant.insert(ignore_if_duplicate=True)
+
+ bom_tree = {
+ template_item.item_code: {
+ "SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
+ "ChildPart5": {},
+ }
+ }
+ template_bom = create_nested_bom(bom_tree, prefix="")
+ variant_bom = make_variant_bom(
+ template_bom.name, template_bom.name, variant.item_code, variant_items=[]
+ )
+ variant_bom.save()
+
+ reqd_order = template_bom.get_tree_representation().level_order_traversal()
+ created_order = variant_bom.get_tree_representation().level_order_traversal()
+
+ self.assertEqual(len(reqd_order), len(created_order))
+
+ for reqd_item, created_item in zip(reqd_order, created_order):
+ self.assertEqual(reqd_item.item_code, created_item.item_code)
+ self.assertEqual(reqd_item.qty, created_item.qty)
+ self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.js b/erpnext/regional/doctype/gst_settings/gst_settings.js
index 808f9bc..cd682c5 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.js
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.js
@@ -35,6 +35,7 @@
return {
filters: {
company: row.company,
+ account_type: "Tax",
is_group: 0
}
};
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 014f409..6ca3d63 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -11,10 +11,11 @@
"hide_custom": 0,
"icon": "settings",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "ERPNext Settings",
"links": [],
- "modified": "2020-12-01 13:38:37.759596",
+ "modified": "2021-06-12 01:58:11.399566",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -109,6 +110,13 @@
"label": "Domain Settings",
"link_to": "Domain Settings",
"type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "icon": "retail",
+ "label": "Products Settings",
+ "link_to": "Products Settings",
+ "type": "DocType"
}
]
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 5ba9c70..41800e3 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -291,7 +291,7 @@
continue
self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
- stock_rbnb, account_currency=warehouse_account_currency, item=d)
+ stock_rbnb, account_currency=warehouse_account_currency, item=d)
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
@@ -318,11 +318,11 @@
(exchange_rate_map[d.purchase_invoice] - self.conversion_rate)
self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, discrepancy_caused_by_exchange_rate_difference,
- remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency, item=d)
- self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
- remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
+ self.add_gl_entry(gl_entries, self.get_company_default("exchange_gain_loss_account"), d.cost_center, discrepancy_caused_by_exchange_rate_difference, 0.0,
+ remarks, self.supplier, debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency, item=d)
# Amount added through landed-cos-voucher
@@ -407,6 +407,7 @@
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked
+ stock_rbnb = self.get_company_default("stock_received_but_not_billed")
i = 1
for tax in self.get("taxes"):
if valuation_tax.get(tax.name):
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index d56822a..dbba21f 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -1054,30 +1054,30 @@
def test_purchase_receipt_with_exchange_rate_difference(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice as create_purchase_invoice
-
- pi = create_purchase_invoice(currency = "USD", conversion_rate = 70)
-
- create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
- properties={"account": '_Test Account Stock In Hand - TCP1'})
+ from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import make_purchase_receipt as create_purchase_receipt
- pr = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
- company="_Test Company with perpetual inventory", currency = "USD", conversion_rate = 80,
- do_not_save = "True")
-
+ pi = create_purchase_invoice(company="_Test Company with perpetual inventory",
+ cost_center = "Main - TCP1",
+ warehouse = "Stores - TCP1",
+ expense_account ="_Test Account Cost for Goods Sold - TCP1",
+ currency = "USD", conversion_rate = 70)
+
+ pr = create_purchase_receipt(pi.name)
+ pr.conversion_rate = 80
pr.items[0].purchase_invoice = pi.name
pr.items[0].purchase_invoice_item = pi.items[0].name
- pr.insert()
+ pr.save()
pr.submit()
- # fetching the latest GL Entry with 'Exchange Gain/Loss - TCP1' account
- gl_entries = frappe.get_all('GL Entry', filters = {'account': 'Exchange Gain/Loss - TCP1'})
- voucher_no = frappe.get_value('GL Entry', gl_entries[0]['name'], 'voucher_no')
- self.assertEqual(pr.name, voucher_no)
+ # Get exchnage gain and loss account
+ exchange_gain_loss_account = frappe.db.get_value('Company', pr.company, 'exchange_gain_loss_account')
- exchange_gain_loss_amount = frappe.get_value('GL Entry', gl_entries[0]['name'], 'debit')
+ # fetching the latest GL Entry with exchange gain and loss account account
+ amount = frappe.db.get_value('GL Entry', {'account': exchange_gain_loss_account, 'voucher_no': pr.name}, 'credit')
discrepancy_caused_by_exchange_rate_diff = abs(pi.items[0].base_net_amount - pr.items[0].base_net_amount)
- self.assertEqual(exchange_gain_loss_amount, discrepancy_caused_by_exchange_rate_diff)
+
+ self.assertEqual(discrepancy_caused_by_exchange_rate_diff, amount)
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index ba31ad7..af2ada8 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -54,7 +54,7 @@
)
# _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
- make_stock_entry(
+ se = make_stock_entry(
item_code="_Test Item for Reposting",
source="Stores - _TC",
target="Finished Goods - _TC",
@@ -64,29 +64,29 @@
posting_date='2020-04-30',
posting_time='14:00'
)
- target_wh_sle = get_previous_sle({
+ target_wh_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-04-30',
- "posting_time": '14:00'
- })
+ "voucher_type": "Stock Entry",
+ "voucher_no": se.name
+ }, ["valuation_rate"], as_dict=1)
self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
# Repack entry on 5-5-2020
repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
- finished_item_sle = get_previous_sle({
+ finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-05-05',
- "posting_time": '14:00'
- })
+ "voucher_type": "Stock Entry",
+ "voucher_no": repack.name
+ }, ["incoming_rate", "valuation_rate"], as_dict=1)
self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
# Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
- create_stock_reconciliation(
+ sr = create_stock_reconciliation(
item_code="_Test Item for Reposting",
warehouse="Stores - _TC",
qty=50,
@@ -109,12 +109,12 @@
self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
# Check valuation rate of repacked item after back-dated entry at Stores
- finished_item_sle = get_previous_sle({
+ finished_item_sle = frappe.db.get_value('Stock Ledger Entry', {
"item_code": "_Test Finished Item for Reposting",
"warehouse": "Finished Goods - _TC",
- "posting_date": '2020-05-05',
- "posting_time": '14:00'
- })
+ "voucher_type": "Stock Entry",
+ "voucher_no": repack.name
+ }, ["incoming_rate", "valuation_rate"], as_dict=1)
self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 2956384..3e15d54 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -357,6 +357,7 @@
if row.current_qty:
data.actual_qty = -1 * row.current_qty
data.qty_after_transaction = flt(row.current_qty)
+ data.previous_qty_after_transaction = flt(row.qty)
data.valuation_rate = flt(row.current_valuation_rate)
data.stock_value = data.qty_after_transaction * data.valuation_rate
data.stock_value_difference = -1 * flt(row.amount_difference)
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 36380b8..7b98c7b 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,7 +6,7 @@
from __future__ import unicode_literals
import frappe, unittest
-from frappe.utils import flt, nowdate, nowtime
+from frappe.utils import flt, nowdate, nowtime, add_days
from erpnext.accounts.utils import get_stock_and_account_balance
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
@@ -14,6 +14,7 @@
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
class TestStockReconciliation(unittest.TestCase):
@classmethod
@@ -204,6 +205,117 @@
self.assertEqual(sr.get("items")[0].valuation_rate, 0)
self.assertEqual(sr.get("items")[0].amount, 0)
+ def test_backdated_stock_reco_qty_reposting(self):
+ """
+ Test if a backdated stock reco recalculates future qty until next reco.
+ -------------------------------------------
+ Var | Doc | Qty | Balance
+ -------------------------------------------
+ SR5 | Reco | 0 | 8 (posting date: today-4) [backdated]
+ PR1 | PR | 10 | 18 (posting date: today-3)
+ PR2 | PR | 1 | 19 (posting date: today-2)
+ SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
+ PR3 | PR | 1 | 7 (posting date: today) # can't post future PR
+ """
+ item_code = "Backdated-Reco-Item"
+ warehouse = "_Test Warehouse - _TC"
+ create_item(item_code)
+
+ pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
+ posting_date=add_days(nowdate(), -3))
+ pr2 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
+ posting_date=add_days(nowdate(), -2))
+ pr3 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
+ posting_date=nowdate())
+
+ pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ self.assertEqual(pr1_balance, 10)
+ self.assertEqual(pr3_balance, 12)
+
+ # post backdated stock reco in between
+ sr4 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=6, rate=100,
+ posting_date=add_days(nowdate(), -1))
+ pr3_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ self.assertEqual(pr3_balance, 7)
+
+ # post backdated stock reco at the start
+ sr5 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=8, rate=100,
+ posting_date=add_days(nowdate(), -4))
+ pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ self.assertEqual(pr1_balance, 18)
+ self.assertEqual(pr2_balance, 19)
+ self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
+
+ # cancel backdated stock reco and check future impact
+ sr5.cancel()
+ pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ pr2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ sr4_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ self.assertEqual(pr1_balance, 10)
+ self.assertEqual(pr2_balance, 11)
+ self.assertEqual(sr4_balance, 6) # check if future stock reco is unaffected
+
+ # teardown
+ sr4.cancel()
+ pr3.cancel()
+ pr2.cancel()
+ pr1.cancel()
+
+ def test_backdated_stock_reco_future_negative_stock(self):
+ """
+ Test if a backdated stock reco causes future negative stock and is blocked.
+ -------------------------------------------
+ Var | Doc | Qty | Balance
+ -------------------------------------------
+ PR1 | PR | 10 | 10 (posting date: today-2)
+ SR3 | Reco | 0 | 1 (posting date: today-1) [backdated & blocked]
+ DN2 | DN | -2 | 8(-1) (posting date: today)
+ """
+ from erpnext.stock.stock_ledger import NegativeStockError
+ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+
+ item_code = "Backdated-Reco-Item"
+ warehouse = "_Test Warehouse - _TC"
+ create_item(item_code)
+
+ negative_stock_setting = frappe.db.get_single_value("Stock Settings", "allow_negative_stock")
+ frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 0)
+
+ pr1 = make_purchase_receipt(item_code=item_code, warehouse=warehouse, qty=10, rate=100,
+ posting_date=add_days(nowdate(), -2))
+ dn2 = create_delivery_note(item_code=item_code, warehouse=warehouse, qty=2, rate=120,
+ posting_date=nowdate())
+
+ pr1_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ dn2_balance = frappe.db.get_value("Stock Ledger Entry", {"voucher_no": dn2.name, "is_cancelled": 0},
+ "qty_after_transaction")
+ self.assertEqual(pr1_balance, 10)
+ self.assertEqual(dn2_balance, 8)
+
+ # check if stock reco is blocked
+ sr3 = create_stock_reconciliation(item_code=item_code, warehouse=warehouse, qty=1, rate=100,
+ posting_date=add_days(nowdate(), -1), do_not_submit=True)
+ self.assertRaises(NegativeStockError, sr3.submit)
+
+ # teardown
+ frappe.db.set_value("Stock Settings", None, "allow_negative_stock", negative_stock_setting)
+ sr3.cancel()
+ dn2.cancel()
+ pr1.cancel()
+
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9fe89c3..4e9c768 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -55,6 +55,11 @@
sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
args = sle_doc.as_dict()
+
+ if sle.get("voucher_type") == "Stock Reconciliation":
+ # preserve previous_qty_after_transaction for qty reposting
+ args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
+
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
def get_args_for_future_sle(row):
@@ -215,7 +220,7 @@
"""
self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse]
- previous_sle = self.get_previous_sle_of_current_voucher(args)
+ previous_sle = get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@@ -227,29 +232,6 @@
"stock_value_difference": 0.0
})
- def get_previous_sle_of_current_voucher(self, args):
- """get stock ledger entries filtered by specific posting datetime conditions"""
-
- args['time_format'] = '%H:%i:%s'
- if not args.get("posting_date"):
- args["posting_date"] = "1900-01-01"
- if not args.get("posting_time"):
- args["posting_time"] = "00:00"
-
- sle = frappe.db.sql("""
- select *, timestamp(posting_date, posting_time) as "timestamp"
- from `tabStock Ledger Entry`
- where item_code = %(item_code)s
- and warehouse = %(warehouse)s
- and is_cancelled = 0
- and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
- order by timestamp(posting_date, posting_time) desc, creation desc
- limit 1
- for update""", args, as_dict=1)
-
- return sle[0] if sle else frappe._dict()
-
-
def build(self):
from erpnext.controllers.stock_controller import future_sle_exists
@@ -734,6 +716,35 @@
bin_doc.flags.via_stock_ledger_entry = True
bin_doc.save(ignore_permissions=True)
+
+def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
+ """get stock ledger entries filtered by specific posting datetime conditions"""
+
+ args['time_format'] = '%H:%i:%s'
+ if not args.get("posting_date"):
+ args["posting_date"] = "1900-01-01"
+ if not args.get("posting_time"):
+ args["posting_time"] = "00:00"
+
+ voucher_condition = ""
+ if exclude_current_voucher:
+ voucher_no = args.get("voucher_no")
+ voucher_condition = f"and voucher_no != '{voucher_no}'"
+
+ sle = frappe.db.sql("""
+ select *, timestamp(posting_date, posting_time) as "timestamp"
+ from `tabStock Ledger Entry`
+ where item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and is_cancelled = 0
+ {voucher_condition}
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+ order by timestamp(posting_date, posting_time) desc, creation desc
+ limit 1
+ for update""".format(voucher_condition=voucher_condition), args, as_dict=1)
+
+ return sle[0] if sle else frappe._dict()
+
def get_previous_sle(args, for_update=False):
"""
get the last sle on or before the current time-bucket,
@@ -862,9 +873,24 @@
return valuation_rate
def update_qty_in_future_sle(args, allow_negative_stock=None):
+ """Recalculate Qty after Transaction in future SLEs based on current SLE."""
+ datetime_limit_condition = ""
+ qty_shift = args.actual_qty
+
+ # find difference/shift in qty caused by stock reconciliation
+ if args.voucher_type == "Stock Reconciliation":
+ qty_shift = get_stock_reco_qty_shift(args)
+
+ # find the next nearest stock reco so that we only recalculate SLEs till that point
+ next_stock_reco_detail = get_next_stock_reco(args)
+ if next_stock_reco_detail:
+ detail = next_stock_reco_detail[0]
+ # add condition to update SLEs before this date & time
+ datetime_limit_condition = get_datetime_limit_condition(detail)
+
frappe.db.sql("""
update `tabStock Ledger Entry`
- set qty_after_transaction = qty_after_transaction + {qty}
+ set qty_after_transaction = qty_after_transaction + {qty_shift}
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
@@ -876,15 +902,70 @@
and creation > %(creation)s
)
)
- """.format(qty=args.actual_qty), args)
+ {datetime_limit_condition}
+ """.format(qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition), args)
validate_negative_qty_in_future_sle(args, allow_negative_stock)
+def get_stock_reco_qty_shift(args):
+ stock_reco_qty_shift = 0
+ if args.get("is_cancelled"):
+ if args.get("previous_qty_after_transaction"):
+ # get qty (balance) that was set at submission
+ last_balance = args.get("previous_qty_after_transaction")
+ stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
+ else:
+ stock_reco_qty_shift = flt(args.actual_qty)
+ else:
+ # reco is being submitted
+ last_balance = get_previous_sle_of_current_voucher(args,
+ exclude_current_voucher=True).get("qty_after_transaction")
+
+ if last_balance is not None:
+ stock_reco_qty_shift = flt(args.qty_after_transaction) - flt(last_balance)
+ else:
+ stock_reco_qty_shift = args.qty_after_transaction
+
+ return stock_reco_qty_shift
+
+def get_next_stock_reco(args):
+ """Returns next nearest stock reconciliaton's details."""
+
+ return frappe.db.sql("""
+ select
+ name, posting_date, posting_time, creation, voucher_no
+ from
+ `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_type = 'Stock Reconciliation'
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+ or (
+ timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+ and creation > %(creation)s
+ )
+ )
+ limit 1
+ """, args, as_dict=1)
+
+def get_datetime_limit_condition(detail):
+ return f"""
+ and
+ (timestamp(posting_date, posting_time) < timestamp('{detail.posting_date}', '{detail.posting_time}')
+ or (
+ timestamp(posting_date, posting_time) = timestamp('{detail.posting_date}', '{detail.posting_time}')
+ and creation < '{detail.creation}'
+ )
+ )"""
+
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
- if args.actual_qty < 0 and not allow_negative_stock:
+ if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock:
sle = get_future_sle_with_negative_qty(args)
if sle:
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
index 2a8446d..0d20b98 100644
--- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
+++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py
@@ -81,10 +81,9 @@
# check SLA custom fields created for leads
sla_fields = get_service_level_agreement_fields()
- meta = frappe.get_meta(doctype, cached=False)
for field in sla_fields:
- self.assertTrue(meta.has_field(field.get("fieldname")))
+ self.assertTrue(frappe.db.exists("Custom Field", {"dt": doctype, "fieldname": field.get("fieldname")}))
def test_docfield_creation_for_sla_on_custom_dt(self):
doctype = create_custom_doctype()
@@ -102,10 +101,9 @@
# check SLA docfields created
sla_fields = get_service_level_agreement_fields()
- meta = frappe.get_meta(doctype.name, cached=False)
for field in sla_fields:
- self.assertTrue(meta.has_field(field.get("fieldname")))
+ self.assertTrue(frappe.db.exists("DocField", {"fieldname": field.get("fieldname"), "parent": doctype.name}))
def test_sla_application(self):
# Default Service Level Agreement
diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py
index 7bfac89..4f45561 100644
--- a/erpnext/www/book_appointment/index.py
+++ b/erpnext/www/book_appointment/index.py
@@ -2,7 +2,7 @@
import datetime
import json
import pytz
-
+from frappe import _
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
@@ -14,7 +14,8 @@
if is_enabled:
return context
else:
- frappe.local.flags.redirect_location = '/404'
+ frappe.redirect_to_message(_("Appointment Scheduling Disabled"), _("Appointment Scheduling has been disabled for this site"),
+ http_status_code=302, indicator_color="red")
raise frappe.Redirect
@frappe.whitelist(allow_guest=True)
@@ -146,4 +147,4 @@
def _datetime_to_deltatime(date_time):
midnight = datetime.datetime.combine(date_time.date(), datetime.time.min)
- return (date_time-midnight)
\ No newline at end of file
+ return (date_time-midnight)