diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
index a9b3d7c4cc2..71a23d30b57 100644
--- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
+++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
@@ -6,8 +6,8 @@ import frappe, json
from frappe import _
from frappe.utils import add_to_date, date_diff, getdate, nowdate, get_last_day, formatdate, get_link_to_form
from erpnext.accounts.report.general_ledger.general_ledger import execute
-from frappe.core.page.dashboard.dashboard import cache_source, get_from_date_from_timespan
-from frappe.desk.doctype.dashboard_chart.dashboard_chart import get_period_ending
+from frappe.core.page.dashboard.dashboard import cache_source
+from frappe.utils.dateutils import get_from_date_from_timespan, get_period_ending
from frappe.utils.nestedset import get_descendants_of
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 9936a6f83da..4db739e09df 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1877,23 +1877,6 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice
- customer_gstin = '27AACCM7806M1Z3'
- customer_gstin_dtls = {
- 'LegalName': '_Test Customer', 'TradeName': '_Test Customer', 'AddrLoc': '_Test City',
- 'StateCode': '27', 'AddrPncd': '410038', 'AddrBno': '_Test Bldg',
- 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
- }
- company_gstin = '27AAECE4835E1ZR'
- company_gstin_dtls = {
- 'LegalName': '_Test Company', 'TradeName': '_Test Company', 'AddrLoc': '_Test City',
- 'StateCode': '27', 'AddrPncd': '401108', 'AddrBno': '_Test Bldg',
- 'AddrBnm': '100', 'AddrFlno': '200', 'AddrSt': '_Test Street'
- }
- # set cache gstin details to avoid fetching details which will require connection to GSP servers
- frappe.local.gstin_cache = {}
- frappe.local.gstin_cache[customer_gstin] = customer_gstin_dtls
- frappe.local.gstin_cache[company_gstin] = company_gstin_dtls
-
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
si.items = []
@@ -1901,8 +1884,8 @@ class TestSalesInvoice(unittest.TestCase):
"item_code": "_Test Item",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
- "qty": 2,
- "rate": 100,
+ "qty": 2000,
+ "rate": 12,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
@@ -1911,31 +1894,50 @@ class TestSalesInvoice(unittest.TestCase):
"item_code": "_Test Item 2",
"uom": "Nos",
"warehouse": "_Test Warehouse - _TC",
- "qty": 4,
- "rate": 150,
+ "qty": 420,
+ "rate": 15,
"income_account": "Sales - _TC",
"expense_account": "Cost of Goods Sold - _TC",
"cost_center": "_Test Cost Center - _TC",
})
+ si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
- total_item_ass_value = sum([d['AssAmt'] for d in einvoice['ItemList']])
- total_item_cgst_value = sum([d['CgstAmt'] for d in einvoice['ItemList']])
- total_item_sgst_value = sum([d['SgstAmt'] for d in einvoice['ItemList']])
- total_item_igst_value = sum([d['IgstAmt'] for d in einvoice['ItemList']])
- total_item_value = sum([d['TotItemVal'] for d in einvoice['ItemList']])
+ total_item_ass_value = 0
+ total_item_cgst_value = 0
+ total_item_sgst_value = 0
+ total_item_igst_value = 0
+ total_item_value = 0
+
+ for item in einvoice['ItemList']:
+ total_item_ass_value += item['AssAmt']
+ total_item_cgst_value += item['CgstAmt']
+ total_item_sgst_value += item['SgstAmt']
+ total_item_igst_value += item['IgstAmt']
+ total_item_value += item['TotItemVal']
+
+ self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount'])
+ self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt'])
+
+ value_details = einvoice['ValDtls']
self.assertEqual(einvoice['Version'], '1.1')
- self.assertEqual(einvoice['ValDtls']['AssVal'], total_item_ass_value)
- self.assertEqual(einvoice['ValDtls']['CgstVal'], total_item_cgst_value)
- self.assertEqual(einvoice['ValDtls']['SgstVal'], total_item_sgst_value)
- self.assertEqual(einvoice['ValDtls']['IgstVal'], total_item_igst_value)
- self.assertEqual(einvoice['ValDtls']['TotInvVal'], total_item_value)
+ self.assertTrue(abs(value_details['AssVal'] - total_item_ass_value) <= 1)
+ self.assertTrue(abs(value_details['CgstVal'] - total_item_cgst_value) <= 1)
+ self.assertTrue(abs(value_details['SgstVal'] - total_item_sgst_value) <= 1)
+ self.assertTrue(abs(value_details['IgstVal'] - total_item_igst_value) <= 1)
+
+ calculated_invoice_value = \
+ value_details['AssVal'] + value_details['CgstVal'] \
+ + value_details['SgstVal'] + value_details['IgstVal'] \
+ + value_details['OthChrg'] - value_details['Discount']
+
+ self.assertTrue(abs(value_details['TotInvVal'] - calculated_invoice_value) <= 1)
self.assertTrue(einvoice['EwbDtls'])
-def make_sales_invoice_for_ewaybill():
+def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
@@ -1983,7 +1985,8 @@ def make_sales_invoice_for_ewaybill():
})
address.save()
-
+
+def make_test_transporter_for_ewaybill():
if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({
"doctype": "Supplier",
@@ -1994,12 +1997,17 @@ def make_sales_invoice_for_ewaybill():
"is_transporter": 1
}).insert()
+def make_sales_invoice_for_ewaybill():
+ make_test_address_for_ewaybill()
+ make_test_transporter_for_ewaybill()
+
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(
"GST Account",
fields=["cgst_account", "sgst_account", "igst_account"],
- filters = {"company": "_Test Company"})
+ filters = {"company": "_Test Company"}
+ )
if not gst_account:
gst_settings.append("gst_accounts", {
@@ -2011,7 +2019,7 @@ def make_sales_invoice_for_ewaybill():
gst_settings.save()
- si = create_sales_invoice(do_not_save =1, rate = '60000')
+ si = create_sales_invoice(do_not_save=1, rate='60000')
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
@@ -2039,27 +2047,6 @@ def make_sales_invoice_for_ewaybill():
return si
- def test_item_tax_validity(self):
- item = frappe.get_doc("Item", "_Test Item 2")
-
- if item.taxes:
- item.taxes = []
- item.save()
-
- item.append("taxes", {
- "item_tax_template": "_Test Item Tax Template 1",
- "valid_from": add_days(nowdate(), 1)
- })
-
- item.save()
-
- sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
- sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1"
- self.assertRaises(frappe.ValidationError, sales_invoice.save)
-
- item.taxes = []
- item.save()
-
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
index b46de6c85bb..429a9f3591d 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
@@ -34,6 +34,9 @@ def valdiate_taxes_and_charges_template(doc):
validate_disabled(doc)
+ # Validate with existing taxes and charges template for unique tax category
+ validate_for_tax_category(doc)
+
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, doc)
@@ -41,3 +44,7 @@ def valdiate_taxes_and_charges_template(doc):
def validate_disabled(doc):
if doc.is_default and doc.disabled:
frappe.throw(_("Disabled template must not be default template"))
+
+def validate_for_tax_category(doc):
+ if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}):
+ frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
index ec9be9aa64e..66981763c12 100644
--- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -156,7 +156,7 @@
{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }} |
{{ frappe.utils.fmt_money(0, None, "INR") }} |
{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }} |
- {{ frappe.utils.fmt_money(0, None, "INR") }} |
+ {{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }} |
{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }} |
{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }} |
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index 96e38a9b779..cce987950b2 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -141,7 +141,7 @@ def get_data(filters):
assets_record = frappe.db.get_all("Asset",
filters=conditions,
- fields=["name", "asset_name", "department", "cost_center", "purchase_receipt",
+ fields=["name as asset_id", "asset_name", "department", "cost_center", "purchase_receipt",
"asset_category", "purchase_date", "gross_purchase_amount", "location",
"available_for_use_date", "status", "purchase_invoice", "opening_accumulated_depreciation"])
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index ee07892fe43..220059796ac 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -1056,7 +1056,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2020-09-14 14:36:12.418690",
+ "modified": "2021-01-22 20:27:11.418690",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index be5763b1d9c..75c28328a00 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -89,7 +89,7 @@ class TestPurchaseOrder(unittest.TestCase):
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
- def test_update_child_qty_rate(self):
+ def test_update_child(self):
mr = make_material_request(qty=10)
po = make_purchase_order(mr.name)
po.supplier = "_Test Supplier"
@@ -119,7 +119,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
- def test_add_new_item_in_update_child_qty_rate(self):
+ def test_update_child_adding_new_item(self):
po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4
po.save()
@@ -145,7 +145,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(po.status, 'To Receive and Bill')
- def test_remove_item_in_update_child_qty_rate(self):
+ def test_update_child_removing_item(self):
po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4
po.save()
@@ -185,7 +185,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEquals(len(po.get('items')), 1)
self.assertEqual(po.status, 'To Receive and Bill')
- def test_update_child_qty_rate_perm(self):
+ def test_update_child_perm(self):
po = create_purchase_order(item_code= "_Test Item", qty=4)
user = 'test@example.com'
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 3c628345a59..69ed1ee8439 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1196,8 +1196,9 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname,
child_item.item_name = item.item_name
child_item.description = item.description
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
+ child_item.uom = trans_item.get("uom") or item.stock_uom
+ conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
+ child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc)
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
@@ -1218,8 +1219,9 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
child_item.item_name = item.item_name
child_item.description = item.description
child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_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
+ child_item.uom = trans_item.get("uom") or item.stock_uom
+ conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
+ child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
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)
@@ -1316,6 +1318,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty"))
prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(d.get("conversion_factor"))
+ prev_uom, new_uom = child_item.get("uom"), d.get("uom")
if parent_doctype == 'Sales Order':
prev_date, new_date = child_item.get("delivery_date"), d.get("delivery_date")
@@ -1324,9 +1327,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
rate_unchanged = prev_rate == new_rate
qty_unchanged = prev_qty == new_qty
+ uom_unchanged = prev_uom == new_uom
conversion_factor_unchanged = prev_con_fac == new_con_fac
date_unchanged = prev_date == new_date if prev_date and new_date else False # in case of delivery note etc
- if rate_unchanged and qty_unchanged and conversion_factor_unchanged and date_unchanged:
+ if rate_unchanged and qty_unchanged and conversion_factor_unchanged and uom_unchanged and date_unchanged:
continue
validate_quantity(child_item, d)
@@ -1347,6 +1351,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
child_item.conversion_factor = 1
else:
child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision)
+
+ if d.get("uom"):
+ child_item.uom = d.get("uom")
+ conversion_factor = flt(get_conversion_factor(child_item.item_code, child_item.uom).get("conversion_factor"))
+ child_item.conversion_factor = flt(d.get('conversion_factor')) or conversion_factor
if d.get("delivery_date") and parent_doctype == 'Sales Order':
child_item.delivery_date = d.get('delivery_date')
@@ -1413,6 +1422,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_receiving_percentage()
if parent.is_subcontracted == "Yes":
parent.update_reserved_qty_for_subcontract()
+ parent.create_raw_materials_supplied("supplied_items")
+ parent.save()
else:
parent.update_reserved_qty()
parent.update_project()
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index b34432ae202..66d0e5f77db 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
@@ -31,11 +31,12 @@ class PlaidConnector():
return access_token
def get_token_request(self, update_mode=False):
+ country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
args = {
"client_name": self.client_name,
# 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", "ES", "FR", "GB", "IE", "NL"],
+ "country_codes": country_codes,
"user": {
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
}
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
index 27062172239..122aa41f4b9 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2018-10-25 10:02:48.656165",
"doctype": "DocType",
"editable_grid": 1,
@@ -11,7 +12,8 @@
"plaid_client_id",
"plaid_secret",
"column_break_7",
- "plaid_env"
+ "plaid_env",
+ "enable_european_access"
],
"fields": [
{
@@ -58,10 +60,17 @@
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_european_access",
+ "fieldtype": "Check",
+ "label": "Enable European Access"
}
],
"issingle": 1,
- "modified": "2020-09-12 02:31:44.542385",
+ "links": [],
+ "modified": "2020-10-29 20:24:56.916104",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
index cf8c9b9ca35..a31cf139358 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
@@ -308,7 +308,7 @@ var calculate_age = function(birth) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)";
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
// List Stock items
diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js
index b60e70fd766..0c3b67a6126 100644
--- a/erpnext/healthcare/doctype/lab_test/lab_test.js
+++ b/erpnext/healthcare/doctype/lab_test/lab_test.js
@@ -293,5 +293,5 @@ var calculate_age = function(birth) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)";
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js
index 9984b0ae9c9..07e13ff4cfd 100644
--- a/erpnext/healthcare/doctype/patient/patient.js
+++ b/erpnext/healthcare/doctype/patient/patient.js
@@ -43,7 +43,7 @@ frappe.ui.form.on('Patient', {
$(frm.fields_dict['age_html'].wrapper).html("");
}
if(frm.doc.dob){
- $(frm.fields_dict['age_html'].wrapper).html("AGE : " + get_age(frm.doc.dob));
+ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`);
}
}
});
@@ -58,7 +58,7 @@ frappe.ui.form.on("Patient", "dob", function(frm) {
}
else{
var age_str = get_age(frm.doc.dob);
- $(frm.fields_dict['age_html'].wrapper).html("AGE : " + age_str);
+ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`);
}
}
else {
@@ -81,7 +81,7 @@ var get_age = function (birth) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)";
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
var btn_create_vital_signs = function (frm) {
diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py
index 420416809dd..fc01e50ff09 100644
--- a/erpnext/healthcare/doctype/patient/patient.py
+++ b/erpnext/healthcare/doctype/patient/patient.py
@@ -58,7 +58,7 @@ class Patient(Document):
if self.dob:
born = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), born)
- age_str = str(age.years) + " year(s) " + str(age.months) + " month(s) " + str(age.days) + " day(s)"
+ age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str
def invoice_patient_registration(self):
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 0a208128564..3ca084ec65f 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -468,5 +468,5 @@ var calculate_age = function(birth) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)";
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
index e6ceb5c0fc3..94e79fb62c1 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
@@ -311,5 +311,5 @@ var calculate_age = function(birth) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)";
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.js b/erpnext/healthcare/doctype/sample_collection/sample_collection.js
index 9934ce48451..32b15aba912 100644
--- a/erpnext/healthcare/doctype/sample_collection/sample_collection.js
+++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.js
@@ -36,5 +36,5 @@ var calculate_age = function(birth) {
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + " Year(s) " + age.getMonth() + " Month(s) " + age.getDate() + " Day(s)";
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 6e2409cf875..4547fc97a31 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -469,6 +469,33 @@ erpnext.utils.update_child_items = function(opts) {
read_only: 0,
disabled: 0,
label: __('Item Code')
+ }, {
+ fieldtype:'Link',
+ fieldname:'uom',
+ options: 'UOM',
+ read_only: 0,
+ label: __('UOM'),
+ reqd: 1,
+ onchange: function () {
+ frappe.call({
+ method: "erpnext.stock.get_item_details.get_conversion_factor",
+ args: { item_code: this.doc.item_code, uom: this.value },
+ callback: r => {
+ if(!r.exc) {
+ if (this.doc.conversion_factor == r.message.conversion_factor) return;
+
+ const docname = this.doc.docname;
+ dialog.fields_dict.trans_items.df.data.some(doc => {
+ if (doc.docname == docname) {
+ doc.conversion_factor = r.message.conversion_factor;
+ dialog.fields_dict.trans_items.grid.refresh();
+ return true;
+ }
+ });
+ }
+ }
+ });
+ },
}, {
fieldtype:'Float',
fieldname:"qty",
@@ -552,6 +579,7 @@ erpnext.utils.update_child_items = function(opts) {
"conversion_factor": d.conversion_factor,
"qty": d.qty,
"rate": d.rate,
+ "uom": d.uom
});
this.data = dialog.fields_dict.trans_items.df.data;
dialog.fields_dict.trans_items.grid.refresh();
diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json
index e5751da5612..60f490d6166 100644
--- a/erpnext/regional/india/e_invoice/einv_template.json
+++ b/erpnext/regional/india/e_invoice/einv_template.json
@@ -59,7 +59,7 @@
{item_list}
],
"ValDtls": {{
- "AssVal": "{invoice_value_details.base_net_total}",
+ "AssVal": "{invoice_value_details.base_total}",
"CgstVal": "{invoice_value_details.total_cgst_amt}",
"SgstVal": "{invoice_value_details.total_sgst_amt}",
"IgstVal": "{invoice_value_details.total_igst_amt}",
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 5ecbb0ff502..9fa1334840d 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -22,6 +22,9 @@ erpnext.setup_einvoice_actions = (doctype) => {
if (!irn && !__unsaved) {
const action = () => {
+ if (frm.doc.__unsaved) {
+ frappe.throw(__('Please save the document to generate IRN.'));
+ }
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
args: { doctype, docname: name },
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index dd98b279019..c6c29ac3fc1 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -11,6 +11,7 @@ import json
import base64
import frappe
import traceback
+import io
from frappe import _, bold
from pyqrcode import create as qrcreate
from frappe.integrations.utils import make_post_request, make_get_request
@@ -161,9 +162,9 @@ def get_item_list(invoice):
item.qty = abs(item.qty)
item.discount_amount = abs(item.discount_amount * item.qty)
- item.unit_rate = abs(item.base_amount / item.qty)
- item.gross_amount = abs(item.base_amount)
- item.taxable_value = abs(item.base_amount)
+ item.unit_rate = abs(item.base_net_amount / item.qty)
+ item.gross_amount = abs(item.base_net_amount)
+ item.taxable_value = abs(item.base_net_amount)
item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
@@ -193,32 +194,39 @@ def update_item_taxes(invoice, item):
item[attr] = 0
for t in invoice.taxes:
+ # this contains item wise tax rate & tax amount (incl. discount)
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
if t.account_head in gst_accounts_list:
+ item_tax_rate = item_tax_detail[0]
+ # item tax amount excluding discount amount
+ item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
+
if t.account_head in gst_accounts.cess_account:
+ item_tax_amount_after_discount = item_tax_detail[1]
if t.charge_type == 'On Item Quantity':
- item.cess_nadv_amount += abs(item_tax_detail[1])
+ item.cess_nadv_amount += abs(item_tax_amount_after_discount)
else:
- item.cess_rate += item_tax_detail[0]
- item.cess_amount += abs(item_tax_detail[1])
- elif t.account_head in gst_accounts.igst_account:
- item.tax_rate += item_tax_detail[0]
- item.igst_amount += abs(item_tax_detail[1])
- elif t.account_head in gst_accounts.sgst_account:
- item.tax_rate += item_tax_detail[0]
- item.sgst_amount += abs(item_tax_detail[1])
- elif t.account_head in gst_accounts.cgst_account:
- item.tax_rate += item_tax_detail[0]
- item.cgst_amount += abs(item_tax_detail[1])
-
+ item.cess_rate += item_tax_rate
+ item.cess_amount += abs(item_tax_amount_after_discount)
+
+ for tax_type in ['igst', 'cgst', 'sgst']:
+ if t.account_head in gst_accounts['{}_account'.format(tax_type)]:
+ item.tax_rate += item_tax_rate
+ item['{}_amount'.format(tax_type)] += abs(item_tax_amount)
+
return item
def get_invoice_value_details(invoice):
invoice_value_details = frappe._dict(dict())
- invoice_value_details.base_net_total = abs(invoice.base_net_total)
- invoice_value_details.invoice_discount_amt = invoice.discount_amount if invoice.discount_amount and invoice.discount_amount > 0 else 0
- # discount amount cannnot be -ve in an e-invoice, so if -ve include discount in round_off
- invoice_value_details.round_off = invoice.rounding_adjustment - (invoice.discount_amount if invoice.discount_amount and invoice.discount_amount < 0 else 0)
+
+ if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
+ invoice_value_details.base_total = abs(invoice.base_total)
+ else:
+ invoice_value_details.base_total = abs(invoice.base_net_total)
+
+ # since tax already considers discount amount
+ invoice_value_details.invoice_discount_amt = 0 # invoice.base_discount_amount
+ invoice_value_details.round_off = invoice.base_rounding_adjustment
invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
@@ -238,13 +246,12 @@ def update_invoice_taxes(invoice, invoice_value_details):
for t in invoice.taxes:
if t.account_head in gst_accounts_list:
if t.account_head in gst_accounts.cess_account:
+ # using after discount amt since item also uses after discount amt for cess calc
invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
- elif t.account_head in gst_accounts.igst_account:
- invoice_value_details.total_igst_amt += abs(t.base_tax_amount_after_discount_amount)
- elif t.account_head in gst_accounts.sgst_account:
- invoice_value_details.total_sgst_amt += abs(t.base_tax_amount_after_discount_amount)
- elif t.account_head in gst_accounts.cgst_account:
- invoice_value_details.total_cgst_amt += abs(t.base_tax_amount_after_discount_amount)
+
+ for tax_type in ['igst', 'cgst', 'sgst']:
+ if t.account_head in gst_accounts['{}_account'.format(tax_type)]:
+ invoice_value_details['total_{}_amt'.format(tax_type)] += abs(t.base_tax_amount_after_discount_amount)
else:
invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
@@ -434,7 +441,7 @@ class GSPConnector():
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
- self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi'
+ self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
def get_credentials(self):
@@ -477,7 +484,7 @@ class GSPConnector():
"data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
"response": json.dumps(res, indent=4) if res else None
})
- request_log.insert(ignore_permissions=True)
+ request_log.save(ignore_permissions=True)
frappe.db.commit()
def fetch_auth_token(self):
@@ -490,7 +497,8 @@ class GSPConnector():
res = self.make_request('post', self.authenticate_url, headers)
self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
- self.e_invoice_settings.save()
+ self.e_invoice_settings.save(ignore_permissions=True)
+ self.e_invoice_settings.reload()
except Exception:
self.log_error(res)
@@ -668,6 +676,8 @@ class GSPConnector():
'cancelRsnCode': reason,
'cancelRmrk': remark
}, indent=4)
+ headers["username"] = headers["user_name"]
+ del headers["user_name"]
try:
res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
@@ -761,25 +771,26 @@ class GSPConnector():
'label': _('IRN Generated')
}
self.update_invoice()
-
+
def attach_qrcode_image(self):
qrcode = self.invoice.signed_qr_code
doctype = self.invoice.doctype
docname = self.invoice.name
+ filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__")
- _file = frappe.new_doc('File')
- _file.update({
- 'file_name': 'QRCode_{}.png'.format(docname.replace('/', '-')),
- 'attached_to_doctype': doctype,
- 'attached_to_name': docname,
- 'content': 'qrcode',
- 'is_private': 1
- })
- _file.insert()
- frappe.db.commit()
+ qr_image = io.BytesIO()
url = qrcreate(qrcode, error='L')
- abs_file_path = os.path.abspath(_file.get_full_path())
- url.png(abs_file_path, scale=2, quiet_zone=1)
+ url.png(qr_image, scale=2, quiet_zone=1)
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": filename,
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "attached_to_field": "qrcode_image",
+ "is_private": 1,
+ "content": qr_image.getvalue()})
+ _file.save()
+ frappe.db.commit()
self.invoice.qrcode_image = _file.file_url
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 37c29befcd7..7aefbd0a127 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -319,7 +319,7 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(get_reserved_qty("_Test Item Home Desktop 100"),
existing_reserved_qty_item2 + 20)
- def test_add_new_item_in_update_child_qty_rate(self):
+ def test_update_child_adding_new_item(self):
so = make_sales_order(item_code= "_Test Item", qty=4)
create_dn_against_so(so.name, 4)
make_sales_invoice(so.name)
@@ -339,7 +339,7 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(so.get("items")[-1].amount, 1400)
self.assertEqual(so.status, 'To Deliver and Bill')
- def test_remove_item_in_update_child_qty_rate(self):
+ def test_update_child_removing_item(self):
so = make_sales_order(**{
"item_list": [{
"item_code": '_Test Item',
@@ -382,7 +382,7 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(so.status, 'To Deliver and Bill')
- def test_update_child_qty_rate(self):
+ def test_update_child(self):
so = make_sales_order(item_code= "_Test Item", qty=4)
create_dn_against_so(so.name, 4)
make_sales_invoice(so.name)
@@ -419,7 +419,7 @@ class TestSalesOrder(unittest.TestCase):
self.assertEqual(so.items[0].rate, 200.34669)
make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency")
- def test_update_child_qty_rate_perm(self):
+ def test_update_child_perm(self):
so = make_sales_order(item_code= "_Test Item", qty=4)
user = 'test@example.com'
@@ -472,7 +472,7 @@ class TestSalesOrder(unittest.TestCase):
workflow.is_active = 0
workflow.save()
- def test_update_child_qty_rate_product_bundle(self):
+ def test_update_child_product_bundle(self):
# test Update Items with product bundle
if not frappe.db.exists("Item", "_Product Bundle Item"):
bundle_item = make_item("_Product Bundle Item", {"is_stock_item": 0})
@@ -492,6 +492,20 @@ class TestSalesOrder(unittest.TestCase):
so.reload()
self.assertEqual(so.packed_items[0].qty, 4)
+ # test uom and conversion factor change
+ update_uom_conv_factor = json.dumps([{
+ 'item_code': so.get("items")[0].item_code,
+ 'rate': so.get("items")[0].rate,
+ 'qty': so.get("items")[0].qty,
+ 'uom': "_Test UOM 1",
+ 'conversion_factor': 2,
+ 'docname': so.get("items")[0].name
+ }])
+ update_child_qty_rate('Sales Order', update_uom_conv_factor, so.name)
+
+ so.reload()
+ self.assertEqual(so.packed_items[0].qty, 8)
+
def test_update_child_with_tax_template(self):
"""
Test Action: Create a SO with one item having its tax account head already in the SO.
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index a804917381f..4919f8b4c05 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -211,7 +211,8 @@ def get_stock_ledger_entries(filters):
from `tabItem` {item_conditions}) item
where item_code = item.name and
company = %(company)s and
- posting_date <= %(to_date)s
+ posting_date <= %(to_date)s and
+ is_cancelled != 1
{sle_conditions}
order by posting_date, posting_time, sle.creation, actual_qty""" #nosec
.format(item_conditions=get_item_conditions(filters),