Merge branch 'develop' into putaway
This commit is contained in:
commit
41ea77ce31
4
.github/helper/documentation.py
vendored
4
.github/helper/documentation.py
vendored
@ -21,8 +21,8 @@ def docs_link_exists(body):
|
|||||||
if word.startswith('http') and uri_validator(word):
|
if word.startswith('http') and uri_validator(word):
|
||||||
parsed_url = urlparse(word)
|
parsed_url = urlparse(word)
|
||||||
if parsed_url.netloc == "github.com":
|
if parsed_url.netloc == "github.com":
|
||||||
_, org, repo, _type, ref = parsed_url.path.split('/')
|
parts = parsed_url.path.split('/')
|
||||||
if org == "frappe" and repo in docs_repos:
|
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
@ -910,98 +910,8 @@
|
|||||||
},
|
},
|
||||||
"is_group": 1
|
"is_group": 1
|
||||||
},
|
},
|
||||||
"Passiva": {
|
"Passiva - Verbindlichkeiten": {
|
||||||
"root_type": "Liability",
|
"root_type": "Liability",
|
||||||
"A - Eigenkapital": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1,
|
|
||||||
"I - Gezeichnetes Kapital": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1,
|
|
||||||
"Gezeichnetes Kapital": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"account_number": "2900"
|
|
||||||
},
|
|
||||||
"Ausstehende Einlagen auf das gezeichnete Kapital": {
|
|
||||||
"account_number": "2910",
|
|
||||||
"is_group": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"II - Kapitalr\u00fccklage": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1,
|
|
||||||
"Kapitalr\u00fccklage": {
|
|
||||||
"account_number": "2920"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"III - Gewinnr\u00fccklagen": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"1 - gesetzliche R\u00fccklage": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1,
|
|
||||||
"Gesetzliche R\u00fccklage": {
|
|
||||||
"account_number": "2930"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1
|
|
||||||
},
|
|
||||||
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1,
|
|
||||||
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
|
|
||||||
"account_number": "2950"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"4 - andere Gewinnr\u00fccklagen": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1,
|
|
||||||
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
|
|
||||||
"is_group": 1,
|
|
||||||
"Gewinnr\u00fccklagen (BilMoG)": {
|
|
||||||
"account_number": "2963"
|
|
||||||
},
|
|
||||||
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
|
|
||||||
"account_number": "2964"
|
|
||||||
},
|
|
||||||
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
|
|
||||||
"account_number": "2965"
|
|
||||||
},
|
|
||||||
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
|
|
||||||
"account_number": "2966"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
|
|
||||||
"account_number": "2967"
|
|
||||||
},
|
|
||||||
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
|
|
||||||
"account_number": "2968"
|
|
||||||
},
|
|
||||||
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
|
|
||||||
"account_number": "2969"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"is_group": 1
|
|
||||||
},
|
|
||||||
"IV - Gewinnvortrag/Verlustvortrag": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1,
|
|
||||||
"Gewinnvortrag vor Verwendung": {
|
|
||||||
"account_number": "2970"
|
|
||||||
},
|
|
||||||
"Verlustvortrag vor Verwendung": {
|
|
||||||
"account_number": "2978"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
|
|
||||||
"account_type": "Equity",
|
|
||||||
"is_group": 1
|
|
||||||
},
|
|
||||||
"Einlagen stiller Gesellschafter": {
|
|
||||||
"account_number": "9295"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"B - R\u00fcckstellungen": {
|
"B - R\u00fcckstellungen": {
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
|
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
|
||||||
@ -1618,6 +1528,143 @@
|
|||||||
},
|
},
|
||||||
"is_group": 1
|
"is_group": 1
|
||||||
},
|
},
|
||||||
|
"Passiva - Eigenkapital": {
|
||||||
|
"root_type": "Equity",
|
||||||
|
"A - Eigenkapital": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1,
|
||||||
|
"I - Gezeichnetes Kapital": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1,
|
||||||
|
"Gezeichnetes Kapital": {
|
||||||
|
"account_number": "2900",
|
||||||
|
"account_type": "Equity"
|
||||||
|
},
|
||||||
|
"Gesch\u00e4ftsguthaben der verbleibenden Mitglieder": {
|
||||||
|
"account_number": "2901"
|
||||||
|
},
|
||||||
|
"Gesch\u00e4ftsguthaben der ausscheidenden Mitglieder": {
|
||||||
|
"account_number": "2902"
|
||||||
|
},
|
||||||
|
"Gesch\u00e4ftsguthaben aus gek\u00fcndigten Gesch\u00e4ftsanteilen": {
|
||||||
|
"account_number": "2903"
|
||||||
|
},
|
||||||
|
"R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
|
||||||
|
"account_number": "2906"
|
||||||
|
},
|
||||||
|
"Gegenkonto R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
|
||||||
|
"account_number": "2907"
|
||||||
|
},
|
||||||
|
"Kapitalerh\u00f6hung aus Gesellschaftsmitteln": {
|
||||||
|
"account_number": "2908"
|
||||||
|
},
|
||||||
|
"Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert": {
|
||||||
|
"account_number": "2910"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"II - Kapitalr\u00fccklage": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1,
|
||||||
|
"Kapitalr\u00fccklage": {
|
||||||
|
"account_number": "2920"
|
||||||
|
},
|
||||||
|
"Kapitalr\u00fccklage durch Ausgabe von Anteilen \u00fcber Nennbetrag": {
|
||||||
|
"account_number": "2925"
|
||||||
|
},
|
||||||
|
"Kapitalr\u00fccklage durch Ausgabe von Schuldverschreibungen": {
|
||||||
|
"account_number": "2926"
|
||||||
|
},
|
||||||
|
"Kapitalr\u00fccklage durch Zuzahlungen gegen Gew\u00e4hrung eines Vorzugs": {
|
||||||
|
"account_number": "2927"
|
||||||
|
},
|
||||||
|
"Kapitalr\u00fccklage durch Zuzahlungen in das Eigenkapital": {
|
||||||
|
"account_number": "2928"
|
||||||
|
},
|
||||||
|
"Nachschusskapital (Gegenkonto 1299)": {
|
||||||
|
"account_number": "2929"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"III - Gewinnr\u00fccklagen": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"1 - gesetzliche R\u00fccklage": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1,
|
||||||
|
"Gesetzliche R\u00fccklage": {
|
||||||
|
"account_number": "2930"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1,
|
||||||
|
"R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
|
||||||
|
"account_number": "2935"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1,
|
||||||
|
"Satzungsm\u00e4\u00dfige R\u00fccklagen": {
|
||||||
|
"account_number": "2950"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"4 - andere Gewinnr\u00fccklagen": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1,
|
||||||
|
"Andere Gewinnr\u00fccklagen": {
|
||||||
|
"account_number": "2960"
|
||||||
|
},
|
||||||
|
"Andere Gewinnr\u00fccklagen aus dem Erwerb eigener Anteile": {
|
||||||
|
"account_number": "2961"
|
||||||
|
},
|
||||||
|
"Eigenkapitalanteil von Wertaufholungen": {
|
||||||
|
"account_number": "2962"
|
||||||
|
},
|
||||||
|
"Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
|
||||||
|
"is_group": 1,
|
||||||
|
"Gewinnr\u00fccklagen (BilMoG)": {
|
||||||
|
"account_number": "2963"
|
||||||
|
},
|
||||||
|
"Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
|
||||||
|
"account_number": "2964"
|
||||||
|
},
|
||||||
|
"Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
|
||||||
|
"account_number": "2965"
|
||||||
|
},
|
||||||
|
"Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
|
||||||
|
"account_number": "2966"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
|
||||||
|
"account_number": "2967"
|
||||||
|
},
|
||||||
|
"Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
|
||||||
|
"account_number": "2968"
|
||||||
|
},
|
||||||
|
"Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
|
||||||
|
"account_number": "2969"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"is_group": 1
|
||||||
|
},
|
||||||
|
"IV - Gewinnvortrag/Verlustvortrag": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1,
|
||||||
|
"Gewinnvortrag vor Verwendung": {
|
||||||
|
"account_number": "2970"
|
||||||
|
},
|
||||||
|
"Verlustvortrag vor Verwendung": {
|
||||||
|
"account_number": "2978"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
|
||||||
|
"account_type": "Equity",
|
||||||
|
"is_group": 1
|
||||||
|
},
|
||||||
|
"Einlagen stiller Gesellschafter": {
|
||||||
|
"account_number": "9295"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"1 - Umsatzerl\u00f6se": {
|
"1 - Umsatzerl\u00f6se": {
|
||||||
"root_type": "Income",
|
"root_type": "Income",
|
||||||
"is_group": 1,
|
"is_group": 1,
|
||||||
|
@ -172,7 +172,7 @@ class TestAccount(unittest.TestCase):
|
|||||||
frappe.delete_doc("Account", doc)
|
frappe.delete_doc("Account", doc)
|
||||||
|
|
||||||
|
|
||||||
def _make_test_records(verbose):
|
def _make_test_records(verbose=None):
|
||||||
from frappe.test_runner import make_test_objects
|
from frappe.test_runner import make_test_objects
|
||||||
|
|
||||||
accounts = [
|
accounts = [
|
||||||
|
@ -159,10 +159,10 @@ class TestBudget(unittest.TestCase):
|
|||||||
|
|
||||||
budget = make_budget(budget_against="Cost Center")
|
budget = make_budget(budget_against="Cost Center")
|
||||||
month = now_datetime().month
|
month = now_datetime().month
|
||||||
if month > 10:
|
if month > 9:
|
||||||
month = 10
|
month = 9
|
||||||
|
|
||||||
for i in range(month):
|
for i in range(month+1):
|
||||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||||
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
|
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
|
||||||
|
|
||||||
@ -181,10 +181,10 @@ class TestBudget(unittest.TestCase):
|
|||||||
|
|
||||||
budget = make_budget(budget_against="Project")
|
budget = make_budget(budget_against="Project")
|
||||||
month = now_datetime().month
|
month = now_datetime().month
|
||||||
if month > 10:
|
if month > 9:
|
||||||
month = 10
|
month = 9
|
||||||
|
|
||||||
for i in range(month):
|
for i in range(month + 1):
|
||||||
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
|
||||||
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project")
|
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project")
|
||||||
|
|
||||||
|
@ -28,22 +28,22 @@ def test_create_test_data():
|
|||||||
"item_group": "_Test Item Group",
|
"item_group": "_Test Item Group",
|
||||||
"item_name": "_Test Tesla Car",
|
"item_name": "_Test Tesla Car",
|
||||||
"apply_warehouse_wise_reorder_level": 0,
|
"apply_warehouse_wise_reorder_level": 0,
|
||||||
"warehouse":"Stores - TCP1",
|
"warehouse":"Stores - _TC",
|
||||||
"gst_hsn_code": "999800",
|
"gst_hsn_code": "999800",
|
||||||
"valuation_rate": 5000,
|
"valuation_rate": 5000,
|
||||||
"standard_rate":5000,
|
"standard_rate":5000,
|
||||||
"item_defaults": [{
|
"item_defaults": [{
|
||||||
"company": "_Test Company with perpetual inventory",
|
"company": "_Test Company",
|
||||||
"default_warehouse": "Stores - TCP1",
|
"default_warehouse": "Stores - _TC",
|
||||||
"default_price_list":"_Test Price List",
|
"default_price_list":"_Test Price List",
|
||||||
"expense_account": "Cost of Goods Sold - TCP1",
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
"buying_cost_center": "Main - TCP1",
|
"buying_cost_center": "Main - _TC",
|
||||||
"selling_cost_center": "Main - TCP1",
|
"selling_cost_center": "Main - _TC",
|
||||||
"income_account": "Sales - TCP1"
|
"income_account": "Sales - _TC"
|
||||||
}],
|
}],
|
||||||
"show_in_website": 1,
|
"show_in_website": 1,
|
||||||
"route":"-test-tesla-car",
|
"route":"-test-tesla-car",
|
||||||
"website_warehouse": "Stores - TCP1"
|
"website_warehouse": "Stores - _TC"
|
||||||
})
|
})
|
||||||
item.insert()
|
item.insert()
|
||||||
# create test item price
|
# create test item price
|
||||||
@ -65,12 +65,12 @@ def test_create_test_data():
|
|||||||
"items": [{
|
"items": [{
|
||||||
"item_code": "_Test Tesla Car"
|
"item_code": "_Test Tesla Car"
|
||||||
}],
|
}],
|
||||||
"warehouse":"Stores - TCP1",
|
"warehouse":"Stores - _TC",
|
||||||
"coupon_code_based":1,
|
"coupon_code_based":1,
|
||||||
"selling": 1,
|
"selling": 1,
|
||||||
"rate_or_discount": "Discount Percentage",
|
"rate_or_discount": "Discount Percentage",
|
||||||
"discount_percentage": 30,
|
"discount_percentage": 30,
|
||||||
"company": "_Test Company with perpetual inventory",
|
"company": "_Test Company",
|
||||||
"currency":"INR",
|
"currency":"INR",
|
||||||
"for_price_list":"_Test Price List"
|
"for_price_list":"_Test Price List"
|
||||||
})
|
})
|
||||||
@ -85,7 +85,7 @@ def test_create_test_data():
|
|||||||
})
|
})
|
||||||
sales_partner.insert()
|
sales_partner.insert()
|
||||||
# create test item coupon code
|
# create test item coupon code
|
||||||
if not frappe.db.exists("Coupon Code","SAVE30"):
|
if not frappe.db.exists("Coupon Code", "SAVE30"):
|
||||||
coupon_code = frappe.get_doc({
|
coupon_code = frappe.get_doc({
|
||||||
"doctype": "Coupon Code",
|
"doctype": "Coupon Code",
|
||||||
"coupon_name":"SAVE30",
|
"coupon_name":"SAVE30",
|
||||||
@ -104,33 +104,25 @@ class TestCouponCode(unittest.TestCase):
|
|||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
def test_1_check_coupon_code_used_before_so(self):
|
def test_sales_order_with_coupon_code(self):
|
||||||
coupon_code = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
|
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
|
||||||
# reset used coupon code count
|
|
||||||
coupon_code.used=0
|
|
||||||
coupon_code.save()
|
|
||||||
# check no coupon code is used before sales order is made
|
|
||||||
self.assertEqual(coupon_code.get("used"),0)
|
|
||||||
|
|
||||||
def test_2_sales_order_with_coupon_code(self):
|
so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
|
||||||
so = make_sales_order(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
|
customer="_Test Customer", selling_price_list="_Test Price List",
|
||||||
customer="_Test Customer", selling_price_list="_Test Price List", item_code="_Test Tesla Car", rate=5000,qty=1,
|
item_code="_Test Tesla Car", rate=5000, qty=1,
|
||||||
do_not_submit=True)
|
do_not_submit=True)
|
||||||
|
|
||||||
so = frappe.get_doc('Sales Order', so.name)
|
|
||||||
# check item price before coupon code is applied
|
|
||||||
self.assertEqual(so.items[0].rate, 5000)
|
self.assertEqual(so.items[0].rate, 5000)
|
||||||
|
|
||||||
so.coupon_code='SAVE30'
|
so.coupon_code='SAVE30'
|
||||||
so.sales_partner='_Test Coupon Partner'
|
so.sales_partner='_Test Coupon Partner'
|
||||||
so.save()
|
so.save()
|
||||||
|
|
||||||
# check item price after coupon code is applied
|
# check item price after coupon code is applied
|
||||||
self.assertEqual(so.items[0].rate, 3500)
|
self.assertEqual(so.items[0].rate, 3500)
|
||||||
|
|
||||||
so.submit()
|
so.submit()
|
||||||
|
self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)
|
||||||
def test_3_check_coupon_code_used_after_so(self):
|
|
||||||
doc = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
|
|
||||||
# check no coupon code is used before sales order is made
|
|
||||||
self.assertEqual(doc.get("used"),1)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -30,20 +30,22 @@ class GLEntry(Document):
|
|||||||
self.pl_must_have_cost_center()
|
self.pl_must_have_cost_center()
|
||||||
self.validate_cost_center()
|
self.validate_cost_center()
|
||||||
|
|
||||||
self.check_pl_account()
|
if not self.flags.from_repost:
|
||||||
self.validate_party()
|
self.check_pl_account()
|
||||||
self.validate_currency()
|
self.validate_party()
|
||||||
|
self.validate_currency()
|
||||||
|
|
||||||
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'):
|
def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
|
||||||
self.validate_account_details(adv_adj)
|
if not from_repost:
|
||||||
self.validate_dimensions_for_pl_and_bs()
|
self.validate_account_details(adv_adj)
|
||||||
|
self.validate_dimensions_for_pl_and_bs()
|
||||||
|
|
||||||
validate_frozen_account(self.account, adv_adj)
|
validate_frozen_account(self.account, adv_adj)
|
||||||
validate_balance_type(self.account, adv_adj)
|
validate_balance_type(self.account, adv_adj)
|
||||||
|
|
||||||
# Update outstanding amt on against voucher
|
# Update outstanding amt on against voucher
|
||||||
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
|
if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
|
||||||
and self.against_voucher and update_outstanding == 'Yes':
|
and self.against_voucher and update_outstanding == 'Yes' and not from_repost:
|
||||||
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
|
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
|
||||||
self.against_voucher)
|
self.against_voucher)
|
||||||
|
|
||||||
@ -106,8 +108,8 @@ class GLEntry(Document):
|
|||||||
from tabAccount where name=%s""", self.account, as_dict=1)[0]
|
from tabAccount where name=%s""", self.account, as_dict=1)[0]
|
||||||
|
|
||||||
if ret.is_group==1:
|
if ret.is_group==1:
|
||||||
frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in
|
frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
|
||||||
transactions''').format(self.voucher_type, self.voucher_no, self.account))
|
.format(self.voucher_type, self.voucher_no, self.account))
|
||||||
|
|
||||||
if ret.docstatus==2:
|
if ret.docstatus==2:
|
||||||
frappe.throw(_("{0} {1}: Account {2} is inactive")
|
frappe.throw(_("{0} {1}: Account {2} is inactive")
|
||||||
@ -136,8 +138,8 @@ class GLEntry(Document):
|
|||||||
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
|
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
|
||||||
|
|
||||||
if self.cost_center and _check_is_group():
|
if self.cost_center and _check_is_group():
|
||||||
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
|
frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""")
|
||||||
be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
|
.format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
|
||||||
|
|
||||||
def validate_party(self):
|
def validate_party(self):
|
||||||
validate_party_frozen_disabled(self.party_type, self.party)
|
validate_party_frozen_disabled(self.party_type, self.party)
|
||||||
|
@ -6,14 +6,18 @@ import frappe, erpnext, json
|
|||||||
from frappe.utils import cstr, flt, fmt_money, formatdate, getdate, nowdate, cint, get_link_to_form
|
from frappe.utils import cstr, flt, fmt_money, formatdate, getdate, nowdate, cint, get_link_to_form
|
||||||
from frappe import msgprint, _, scrub
|
from frappe import msgprint, _, scrub
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
from erpnext.accounts.utils import get_balance_on, get_account_currency
|
from erpnext.accounts.utils import get_balance_on, get_stock_accounts, get_stock_and_account_balance, \
|
||||||
|
get_account_currency, check_if_stock_and_account_balance_synced
|
||||||
from erpnext.accounts.party import get_party_account
|
from erpnext.accounts.party import get_party_account
|
||||||
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
|
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
|
||||||
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
|
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \
|
||||||
|
import get_party_account_based_on_invoice_discounting
|
||||||
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
||||||
|
|
||||||
from six import string_types, iteritems
|
from six import string_types, iteritems
|
||||||
|
|
||||||
|
class StockAccountInvalidTransaction(frappe.ValidationError): pass
|
||||||
|
|
||||||
class JournalEntry(AccountsController):
|
class JournalEntry(AccountsController):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(JournalEntry, self).__init__(*args, **kwargs)
|
super(JournalEntry, self).__init__(*args, **kwargs)
|
||||||
@ -46,6 +50,7 @@ class JournalEntry(AccountsController):
|
|||||||
self.validate_empty_accounts_table()
|
self.validate_empty_accounts_table()
|
||||||
self.set_account_and_party_balance()
|
self.set_account_and_party_balance()
|
||||||
self.validate_inter_company_accounts()
|
self.validate_inter_company_accounts()
|
||||||
|
self.validate_stock_accounts()
|
||||||
if not self.title:
|
if not self.title:
|
||||||
self.title = self.get_title()
|
self.title = self.get_title()
|
||||||
|
|
||||||
@ -57,6 +62,8 @@ class JournalEntry(AccountsController):
|
|||||||
self.update_expense_claim()
|
self.update_expense_claim()
|
||||||
self.update_inter_company_jv()
|
self.update_inter_company_jv()
|
||||||
self.update_invoice_discounting()
|
self.update_invoice_discounting()
|
||||||
|
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||||
|
self.company, self.doctype, self.name)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
|
||||||
@ -96,6 +103,16 @@ class JournalEntry(AccountsController):
|
|||||||
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
|
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
|
||||||
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
|
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
|
||||||
|
|
||||||
|
def validate_stock_accounts(self):
|
||||||
|
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
|
||||||
|
for account in stock_accounts:
|
||||||
|
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
|
||||||
|
self.posting_date, self.company)
|
||||||
|
|
||||||
|
if account_bal == stock_bal:
|
||||||
|
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
||||||
|
.format(account), StockAccountInvalidTransaction)
|
||||||
|
|
||||||
def update_inter_company_jv(self):
|
def update_inter_company_jv(self):
|
||||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||||
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
|
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
|
||||||
|
@ -6,7 +6,7 @@ import unittest, frappe
|
|||||||
from frappe.utils import flt, nowdate
|
from frappe.utils import flt, nowdate
|
||||||
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
from erpnext.accounts.doctype.account.test_account import get_inventory_account
|
||||||
from erpnext.exceptions import InvalidAccountCurrency
|
from erpnext.exceptions import InvalidAccountCurrency
|
||||||
from erpnext.accounts.general_ledger import StockAccountInvalidTransaction
|
from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
|
||||||
|
|
||||||
class TestJournalEntry(unittest.TestCase):
|
class TestJournalEntry(unittest.TestCase):
|
||||||
def test_journal_entry_with_against_jv(self):
|
def test_journal_entry_with_against_jv(self):
|
||||||
@ -75,54 +75,46 @@ class TestJournalEntry(unittest.TestCase):
|
|||||||
|
|
||||||
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
|
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
|
||||||
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
|
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
|
||||||
|
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
||||||
|
"unlink_advance_payment_on_cancelation_of_order", 0)
|
||||||
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
|
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
|
||||||
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
|
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
|
||||||
|
|
||||||
def test_jv_against_stock_account(self):
|
def test_jv_against_stock_account(self):
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
company = "_Test Company with perpetual inventory"
|
||||||
set_perpetual_inventory()
|
stock_account = get_inventory_account(company)
|
||||||
|
|
||||||
jv = frappe.copy_doc({
|
from erpnext.accounts.utils import get_stock_and_account_balance
|
||||||
"cheque_date": nowdate(),
|
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
|
||||||
"cheque_no": "33",
|
diff = flt(account_bal) - flt(stock_bal)
|
||||||
"company": "_Test Company with perpetual inventory",
|
|
||||||
"doctype": "Journal Entry",
|
|
||||||
"accounts": [
|
|
||||||
{
|
|
||||||
"account": "Debtors - TCP1",
|
|
||||||
"party_type": "Customer",
|
|
||||||
"party": "_Test Customer",
|
|
||||||
"credit_in_account_currency": 400.0,
|
|
||||||
"debit_in_account_currency": 0.0,
|
|
||||||
"doctype": "Journal Entry Account",
|
|
||||||
"parentfield": "accounts",
|
|
||||||
"cost_center": "Main - TCP1"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"account": "_Test Bank - TCP1",
|
|
||||||
"credit_in_account_currency": 0.0,
|
|
||||||
"debit_in_account_currency": 400.0,
|
|
||||||
"doctype": "Journal Entry Account",
|
|
||||||
"parentfield": "accounts",
|
|
||||||
"cost_center": "Main - TCP1"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"naming_series": "_T-Journal Entry-",
|
|
||||||
"posting_date": nowdate(),
|
|
||||||
"user_remark": "test",
|
|
||||||
"voucher_type": "Bank Entry"
|
|
||||||
})
|
|
||||||
|
|
||||||
jv.get("accounts")[0].update({
|
if not diff:
|
||||||
"account": get_inventory_account('_Test Company with perpetual inventory'),
|
diff = 100
|
||||||
"company": "_Test Company with perpetual inventory",
|
|
||||||
"party_type": None,
|
jv = frappe.new_doc("Journal Entry")
|
||||||
"party": None
|
jv.company = company
|
||||||
|
jv.posting_date = nowdate()
|
||||||
|
jv.append("accounts", {
|
||||||
|
"account": stock_account,
|
||||||
|
"cost_center": "Main - TCP1",
|
||||||
|
"debit_in_account_currency": 0 if diff > 0 else abs(diff),
|
||||||
|
"credit_in_account_currency": diff if diff > 0 else 0
|
||||||
})
|
})
|
||||||
|
|
||||||
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
|
jv.append("accounts", {
|
||||||
jv.cancel()
|
"account": "Stock Adjustment - TCP1",
|
||||||
set_perpetual_inventory(0)
|
"cost_center": "Main - TCP1",
|
||||||
|
"debit_in_account_currency": diff if diff > 0 else 0,
|
||||||
|
"credit_in_account_currency": 0 if diff > 0 else abs(diff)
|
||||||
|
})
|
||||||
|
jv.insert()
|
||||||
|
|
||||||
|
if account_bal == stock_bal:
|
||||||
|
self.assertRaises(StockAccountInvalidTransaction, jv.submit)
|
||||||
|
frappe.db.rollback()
|
||||||
|
else:
|
||||||
|
jv.submit()
|
||||||
|
jv.cancel()
|
||||||
|
|
||||||
def test_multi_currency(self):
|
def test_multi_currency(self):
|
||||||
jv = make_journal_entry("_Test Bank USD - _TC",
|
jv = make_journal_entry("_Test Bank USD - _TC",
|
||||||
|
@ -8,12 +8,10 @@ import unittest
|
|||||||
from frappe.utils import today, cint, flt, getdate
|
from frappe.utils import today, cint, flt, getdate
|
||||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
|
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
|
||||||
from erpnext.accounts.party import get_dashboard_info
|
from erpnext.accounts.party import get_dashboard_info
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
|
||||||
|
|
||||||
class TestLoyaltyProgram(unittest.TestCase):
|
class TestLoyaltyProgram(unittest.TestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
set_perpetual_inventory(0)
|
|
||||||
# create relevant item, customer, loyalty program, etc
|
# create relevant item, customer, loyalty program, etc
|
||||||
create_records()
|
create_records()
|
||||||
|
|
||||||
|
@ -267,6 +267,8 @@ class POSInvoice(SalesInvoice):
|
|||||||
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
|
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
|
||||||
if not self.pos_profile:
|
if not self.pos_profile:
|
||||||
pos_profile = get_pos_profile(self.company) or {}
|
pos_profile = get_pos_profile(self.company) or {}
|
||||||
|
if not pos_profile:
|
||||||
|
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
|
||||||
self.pos_profile = pos_profile.get('name')
|
self.pos_profile = pos_profile.get('name')
|
||||||
|
|
||||||
profile = {}
|
profile = {}
|
||||||
|
@ -345,9 +345,13 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
|
|||||||
if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency)
|
if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency)
|
||||||
or (pricing_rule.margin_type == 'Percentage')):
|
or (pricing_rule.margin_type == 'Percentage')):
|
||||||
item_details.margin_type = pricing_rule.margin_type
|
item_details.margin_type = pricing_rule.margin_type
|
||||||
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
|
|
||||||
item_details.has_margin = True
|
item_details.has_margin = True
|
||||||
|
|
||||||
|
if pricing_rule.apply_multiple_pricing_rules and item_details.margin_rate_or_amount is not None:
|
||||||
|
item_details.margin_rate_or_amount += pricing_rule.margin_rate_or_amount
|
||||||
|
else:
|
||||||
|
item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
|
||||||
|
|
||||||
if pricing_rule.rate_or_discount == 'Rate':
|
if pricing_rule.rate_or_discount == 'Rate':
|
||||||
pricing_rule_rate = 0.0
|
pricing_rule_rate = 0.0
|
||||||
if pricing_rule.currency == args.currency:
|
if pricing_rule.currency == args.currency:
|
||||||
|
@ -164,7 +164,15 @@ def _get_tree_conditions(args, parenttype, table, allow_blank=True):
|
|||||||
frappe.throw(_("Invalid {0}").format(args.get(field)))
|
frappe.throw(_("Invalid {0}").format(args.get(field)))
|
||||||
|
|
||||||
parent_groups = frappe.db.sql_list("""select name from `tab%s`
|
parent_groups = frappe.db.sql_list("""select name from `tab%s`
|
||||||
where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
|
where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
|
||||||
|
|
||||||
|
if parenttype in ["Customer Group", "Item Group", "Territory"]:
|
||||||
|
parent_field = "parent_{0}".format(frappe.scrub(parenttype))
|
||||||
|
root_name = frappe.db.get_list(parenttype,
|
||||||
|
{"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1)
|
||||||
|
|
||||||
|
if root_name and root_name[0][0]:
|
||||||
|
parent_groups.append(root_name[0][0])
|
||||||
|
|
||||||
if parent_groups:
|
if parent_groups:
|
||||||
if allow_blank: parent_groups.append('')
|
if allow_blank: parent_groups.append('')
|
||||||
|
@ -410,10 +410,13 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# this sequence because outstanding may get -negative
|
# this sequence because outstanding may get -negative
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
|
|
||||||
|
if self.update_stock == 1:
|
||||||
|
self.repost_future_sle_and_gle()
|
||||||
|
|
||||||
self.update_project()
|
self.update_project()
|
||||||
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
if not gl_entries:
|
if not gl_entries:
|
||||||
gl_entries = self.get_gl_entries()
|
gl_entries = self.get_gl_entries()
|
||||||
|
|
||||||
@ -421,7 +424,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
|
||||||
|
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
|
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
|
||||||
@ -436,9 +439,11 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
||||||
if self.auto_accounting_for_stock:
|
if self.auto_accounting_for_stock:
|
||||||
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
|
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
|
||||||
|
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
||||||
else:
|
else:
|
||||||
self.stock_received_but_not_billed = None
|
self.stock_received_but_not_billed = None
|
||||||
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
self.expenses_included_in_valuation = None
|
||||||
|
|
||||||
self.negative_expense_to_be_booked = 0.0
|
self.negative_expense_to_be_booked = 0.0
|
||||||
gl_entries = []
|
gl_entries = []
|
||||||
|
|
||||||
@ -994,11 +999,15 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.delete_auto_created_batches()
|
self.delete_auto_created_batches()
|
||||||
|
|
||||||
self.make_gl_entries_on_cancel()
|
self.make_gl_entries_on_cancel()
|
||||||
|
|
||||||
|
if self.update_stock == 1:
|
||||||
|
self.repost_future_sle_and_gle()
|
||||||
|
|
||||||
self.update_project()
|
self.update_project()
|
||||||
frappe.db.set(self, 'status', 'Cancelled')
|
frappe.db.set(self, 'status', 'Cancelled')
|
||||||
|
|
||||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
|
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
|
||||||
|
|
||||||
def update_project(self):
|
def update_project(self):
|
||||||
project_list = []
|
project_list = []
|
||||||
|
@ -9,8 +9,7 @@ import frappe.model
|
|||||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||||
from frappe.utils import cint, flt, today, nowdate, add_days, getdate
|
from frappe.utils import cint, flt, today, nowdate, add_days, getdate
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt, get_taxes
|
||||||
test_records as pr_test_records, make_purchase_receipt, get_taxes
|
|
||||||
from erpnext.controllers.accounts_controller import get_payment_terms
|
from erpnext.controllers.accounts_controller import get_payment_terms
|
||||||
from erpnext.exceptions import InvalidCurrency
|
from erpnext.exceptions import InvalidCurrency
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
|
||||||
@ -33,13 +32,10 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_gl_entries_without_perpetual_inventory(self):
|
def test_gl_entries_without_perpetual_inventory(self):
|
||||||
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
|
||||||
wrapper = frappe.copy_doc(test_records[0])
|
pi = frappe.copy_doc(test_records[0])
|
||||||
set_perpetual_inventory(0, wrapper.company)
|
self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(pi.company)))
|
||||||
self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(wrapper.company)))
|
pi.insert()
|
||||||
wrapper.insert()
|
pi.submit()
|
||||||
wrapper.submit()
|
|
||||||
wrapper.load_from_db()
|
|
||||||
dl = wrapper
|
|
||||||
|
|
||||||
expected_gl_entries = {
|
expected_gl_entries = {
|
||||||
"_Test Payable - _TC": [0, 1512.0],
|
"_Test Payable - _TC": [0, 1512.0],
|
||||||
@ -54,12 +50,16 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
"Round Off - _TC": [0, 0.3]
|
"Round Off - _TC": [0, 0.3]
|
||||||
}
|
}
|
||||||
gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
||||||
where voucher_type = 'Purchase Invoice' and voucher_no = %s""", dl.name, as_dict=1)
|
where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1)
|
||||||
for d in gl_entries:
|
for d in gl_entries:
|
||||||
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
|
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
|
||||||
|
|
||||||
def test_gl_entries_with_perpetual_inventory(self):
|
def test_gl_entries_with_perpetual_inventory(self):
|
||||||
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10)
|
pi = make_purchase_invoice(company="_Test Company with perpetual inventory",
|
||||||
|
warehouse= "Stores - TCP1", cost_center = "Main - TCP1",
|
||||||
|
expense_account ="_Test Account Cost for Goods Sold - TCP1",
|
||||||
|
get_taxes_and_charges=True, qty=10)
|
||||||
|
|
||||||
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
|
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
|
||||||
|
|
||||||
self.check_gle_for_pi(pi.name)
|
self.check_gle_for_pi(pi.name)
|
||||||
@ -198,8 +198,6 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
|
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
|
||||||
|
|
||||||
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
|
|
||||||
|
|
||||||
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
|
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
|
||||||
|
|
||||||
for d in pi.items:
|
for d in pi.items:
|
||||||
@ -247,17 +245,11 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
|
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
|
||||||
|
|
||||||
def test_gl_entries_with_aia_for_non_stock_items(self):
|
def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self):
|
||||||
pi = frappe.copy_doc(test_records[1])
|
pi = make_purchase_invoice(item_code = "_Test Non Stock Item",
|
||||||
set_perpetual_inventory(1, pi.company)
|
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
|
||||||
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
|
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
|
||||||
pi.get("items")[0].item_code = "_Test Non Stock Item"
|
|
||||||
pi.get("items")[0].expense_account = "_Test Account Cost for Goods Sold - _TC"
|
|
||||||
pi.get("taxes").pop(0)
|
|
||||||
pi.get("taxes").pop(1)
|
|
||||||
pi.insert()
|
|
||||||
pi.submit()
|
|
||||||
pi.load_from_db()
|
|
||||||
self.assertTrue(pi.status, "Unpaid")
|
self.assertTrue(pi.status, "Unpaid")
|
||||||
|
|
||||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||||
@ -265,17 +257,15 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
order by account asc""", pi.name, as_dict=1)
|
order by account asc""", pi.name, as_dict=1)
|
||||||
self.assertTrue(gl_entries)
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
expected_values = sorted([
|
expected_values = [
|
||||||
["_Test Payable - _TC", 0, 620],
|
["_Test Account Cost for Goods Sold - TCP1", 250.0, 0],
|
||||||
["_Test Account Cost for Goods Sold - _TC", 500.0, 0],
|
["Creditors - TCP1", 0, 250]
|
||||||
["_Test Account VAT - _TC", 120.0, 0],
|
]
|
||||||
])
|
|
||||||
|
|
||||||
for i, gle in enumerate(gl_entries):
|
for i, gle in enumerate(gl_entries):
|
||||||
self.assertEqual(expected_values[i][0], gle.account)
|
self.assertEqual(expected_values[i][0], gle.account)
|
||||||
self.assertEqual(expected_values[i][1], gle.debit)
|
self.assertEqual(expected_values[i][1], gle.debit)
|
||||||
self.assertEqual(expected_values[i][2], gle.credit)
|
self.assertEqual(expected_values[i][2], gle.credit)
|
||||||
set_perpetual_inventory(0, pi.company)
|
|
||||||
|
|
||||||
def test_purchase_invoice_calculation(self):
|
def test_purchase_invoice_calculation(self):
|
||||||
pi = frappe.copy_doc(test_records[0])
|
pi = frappe.copy_doc(test_records[0])
|
||||||
@ -457,12 +447,13 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
pi.cancel()
|
pi.cancel()
|
||||||
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
|
self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
|
||||||
|
|
||||||
def test_return_purchase_invoice(self):
|
def test_return_purchase_invoice_with_perpetual_inventory(self):
|
||||||
set_perpetual_inventory()
|
pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
|
||||||
|
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
|
||||||
|
|
||||||
pi = make_purchase_invoice()
|
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
|
||||||
|
company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
|
||||||
return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2)
|
cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
|
||||||
|
|
||||||
|
|
||||||
# check gl entries for return
|
# check gl entries for return
|
||||||
@ -473,19 +464,15 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
self.assertTrue(gl_entries)
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
expected_values = {
|
expected_values = {
|
||||||
"Creditors - _TC": [100.0, 0.0],
|
"Creditors - TCP1": [100.0, 0.0],
|
||||||
"Stock Received But Not Billed - _TC": [0.0, 100.0],
|
"Stock Received But Not Billed - TCP1": [0.0, 100.0],
|
||||||
}
|
}
|
||||||
|
|
||||||
for gle in gl_entries:
|
for gle in gl_entries:
|
||||||
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
self.assertEqual(expected_values[gle.account][0], gle.debit)
|
||||||
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
||||||
|
|
||||||
set_perpetual_inventory(0)
|
|
||||||
|
|
||||||
def test_multi_currency_gle(self):
|
def test_multi_currency_gle(self):
|
||||||
set_perpetual_inventory(0)
|
|
||||||
|
|
||||||
pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
|
pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
|
||||||
currency="USD", conversion_rate=50)
|
currency="USD", conversion_rate=50)
|
||||||
|
|
||||||
@ -640,10 +627,9 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(len(pi.get("supplied_items")), 2)
|
self.assertEqual(len(pi.get("supplied_items")), 2)
|
||||||
|
|
||||||
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
|
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
|
||||||
self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
|
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
|
||||||
|
|
||||||
def test_rejected_serial_no(self):
|
def test_rejected_serial_no(self):
|
||||||
set_perpetual_inventory(0)
|
|
||||||
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
|
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
|
||||||
rejected_qty=1, rate=500, update_stock=1,
|
rejected_qty=1, rate=500, update_stock=1,
|
||||||
rejected_warehouse = "_Test Rejected Warehouse - _TC")
|
rejected_warehouse = "_Test Rejected Warehouse - _TC")
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
{% include "erpnext/regional/india/taxes.js" %}
|
{% include "erpnext/regional/india/taxes.js" %}
|
||||||
|
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
|
||||||
|
|
||||||
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
||||||
|
erpnext.setup_einvoice_actions('Sales Invoice')
|
||||||
|
|
||||||
frappe.ui.form.on("Sales Invoice", {
|
frappe.ui.form.on("Sales Invoice", {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe, erpnext
|
import frappe, erpnext
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form
|
from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
|
||||||
from frappe import _, msgprint, throw
|
from frappe import _, msgprint, throw
|
||||||
from erpnext.accounts.party import get_party_account, get_due_date
|
from erpnext.accounts.party import get_party_account, get_due_date
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
@ -180,6 +180,9 @@ class SalesInvoice(SellingController):
|
|||||||
# this sequence because outstanding may get -ve
|
# this sequence because outstanding may get -ve
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
|
|
||||||
|
if self.update_stock == 1:
|
||||||
|
self.repost_future_sle_and_gle()
|
||||||
|
|
||||||
if not self.is_return:
|
if not self.is_return:
|
||||||
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
|
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
|
||||||
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
|
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
|
||||||
@ -229,9 +232,9 @@ class SalesInvoice(SellingController):
|
|||||||
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
frappe.throw(_("At least one mode of payment is required for POS invoice."))
|
||||||
|
|
||||||
def before_cancel(self):
|
def before_cancel(self):
|
||||||
|
super(SalesInvoice, self).before_cancel()
|
||||||
self.update_time_sheet(None)
|
self.update_time_sheet(None)
|
||||||
|
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
super(SalesInvoice, self).on_cancel()
|
super(SalesInvoice, self).on_cancel()
|
||||||
|
|
||||||
@ -258,6 +261,10 @@ class SalesInvoice(SellingController):
|
|||||||
self.update_stock_ledger()
|
self.update_stock_ledger()
|
||||||
|
|
||||||
self.make_gl_entries_on_cancel()
|
self.make_gl_entries_on_cancel()
|
||||||
|
|
||||||
|
if self.update_stock == 1:
|
||||||
|
self.repost_future_sle_and_gle()
|
||||||
|
|
||||||
frappe.db.set(self, 'status', 'Cancelled')
|
frappe.db.set(self, 'status', 'Cancelled')
|
||||||
|
|
||||||
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
|
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
|
||||||
@ -279,7 +286,7 @@ class SalesInvoice(SellingController):
|
|||||||
if "Healthcare" in active_domains:
|
if "Healthcare" in active_domains:
|
||||||
manage_invoice_submit_cancel(self, "on_cancel")
|
manage_invoice_submit_cancel(self, "on_cancel")
|
||||||
|
|
||||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
|
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
|
||||||
|
|
||||||
def update_status_updater_args(self):
|
def update_status_updater_args(self):
|
||||||
if cint(self.update_stock):
|
if cint(self.update_stock):
|
||||||
@ -542,7 +549,12 @@ class SalesInvoice(SellingController):
|
|||||||
self.against_income_account = ','.join(against_acc)
|
self.against_income_account = ','.join(against_acc)
|
||||||
|
|
||||||
def add_remarks(self):
|
def add_remarks(self):
|
||||||
if not self.remarks: self.remarks = 'No Remarks'
|
if not self.remarks:
|
||||||
|
if self.po_no and self.po_date:
|
||||||
|
self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
|
||||||
|
formatdate(self.po_date))
|
||||||
|
else:
|
||||||
|
self.remarks = _("No Remarks")
|
||||||
|
|
||||||
def validate_auto_set_posting_time(self):
|
def validate_auto_set_posting_time(self):
|
||||||
# Don't auto set the posting date and time if invoice is amended
|
# Don't auto set the posting date and time if invoice is amended
|
||||||
@ -722,22 +734,20 @@ class SalesInvoice(SellingController):
|
|||||||
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
|
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
|
||||||
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
|
||||||
|
|
||||||
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
|
||||||
if not gl_entries:
|
if not gl_entries:
|
||||||
gl_entries = self.get_gl_entries()
|
gl_entries = self.get_gl_entries()
|
||||||
|
|
||||||
if gl_entries:
|
if gl_entries:
|
||||||
from erpnext.accounts.general_ledger import make_gl_entries
|
|
||||||
|
|
||||||
# if POS and amount is written off, updating outstanding amt after posting all gl entries
|
# if POS and amount is written off, updating outstanding amt after posting all gl entries
|
||||||
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
|
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
|
||||||
cint(self.redeem_loyalty_points)) else "Yes"
|
cint(self.redeem_loyalty_points)) else "Yes"
|
||||||
|
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
|
make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
|
||||||
elif self.docstatus == 2:
|
elif self.docstatus == 2:
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
"description": "138-CMS Shoe",
|
"description": "138-CMS Shoe",
|
||||||
"doctype": "Sales Invoice Item",
|
"doctype": "Sales Invoice Item",
|
||||||
"income_account": "Sales - _TC",
|
"income_account": "Sales - _TC",
|
||||||
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
"expense_account": "_Test Account Cost for Goods Sold - _TC",
|
||||||
|
"item_code": "138-CMS Shoe",
|
||||||
"item_name": "138-CMS Shoe",
|
"item_name": "138-CMS Shoe",
|
||||||
"parentfield": "items",
|
"parentfield": "items",
|
||||||
"qty": 1.0,
|
"qty": 1.0,
|
||||||
|
@ -10,7 +10,6 @@ from frappe.model.dynamic_links import get_dynamic_link_map
|
|||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
|
||||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
|
||||||
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
|
||||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
|
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
|
||||||
from frappe.model.naming import make_autoname
|
from frappe.model.naming import make_autoname
|
||||||
@ -659,7 +658,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
|
|
||||||
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
|
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
|
||||||
si = frappe.copy_doc(test_records[1])
|
si = frappe.copy_doc(test_records[1])
|
||||||
set_perpetual_inventory(0, si.company)
|
|
||||||
si.insert()
|
si.insert()
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
@ -815,7 +813,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
frappe.db.sql("delete from `tabPOS Profile`")
|
frappe.db.sql("delete from `tabPOS Profile`")
|
||||||
|
|
||||||
def test_pos_si_without_payment(self):
|
def test_pos_si_without_payment(self):
|
||||||
set_perpetual_inventory()
|
|
||||||
make_pos_profile()
|
make_pos_profile()
|
||||||
|
|
||||||
pos = copy.deepcopy(test_records[1])
|
pos = copy.deepcopy(test_records[1])
|
||||||
@ -829,9 +826,8 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, si.submit)
|
self.assertRaises(frappe.ValidationError, si.submit)
|
||||||
|
|
||||||
def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self):
|
def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self):
|
||||||
set_perpetual_inventory()
|
si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1",
|
||||||
|
income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True)
|
||||||
si = frappe.get_doc(test_records[1])
|
|
||||||
si.get("items")[0].item_code = None
|
si.get("items")[0].item_code = None
|
||||||
si.insert()
|
si.insert()
|
||||||
si.submit()
|
si.submit()
|
||||||
@ -842,24 +838,16 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertTrue(gl_entries)
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
expected_values = dict((d[0], d) for d in [
|
expected_values = dict((d[0], d) for d in [
|
||||||
[si.debit_to, 630.0, 0.0],
|
["Debtors - TCP1", 100.0, 0.0],
|
||||||
[test_records[1]["items"][0]["income_account"], 0.0, 500.0],
|
["Sales - TCP1", 0.0, 100.0]
|
||||||
[test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
|
|
||||||
[test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
|
|
||||||
])
|
])
|
||||||
for i, gle in enumerate(gl_entries):
|
for i, gle in enumerate(gl_entries):
|
||||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||||
|
|
||||||
set_perpetual_inventory(0)
|
|
||||||
|
|
||||||
def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self):
|
def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self):
|
||||||
set_perpetual_inventory()
|
si = create_sales_invoice(item="_Test Non Stock Item")
|
||||||
si = frappe.get_doc(test_records[1])
|
|
||||||
si.get("items")[0].item_code = "_Test Non Stock Item"
|
|
||||||
si.insert()
|
|
||||||
si.submit()
|
|
||||||
|
|
||||||
gl_entries = frappe.db.sql("""select account, debit, credit
|
gl_entries = frappe.db.sql("""select account, debit, credit
|
||||||
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
|
||||||
@ -867,17 +855,14 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertTrue(gl_entries)
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
expected_values = dict((d[0], d) for d in [
|
expected_values = dict((d[0], d) for d in [
|
||||||
[si.debit_to, 630.0, 0.0],
|
[si.debit_to, 100.0, 0.0],
|
||||||
[test_records[1]["items"][0]["income_account"], 0.0, 500.0],
|
[test_records[1]["items"][0]["income_account"], 0.0, 100.0]
|
||||||
[test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
|
|
||||||
[test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
|
|
||||||
])
|
])
|
||||||
for i, gle in enumerate(gl_entries):
|
for i, gle in enumerate(gl_entries):
|
||||||
self.assertEqual(expected_values[gle.account][0], gle.account)
|
self.assertEqual(expected_values[gle.account][0], gle.account)
|
||||||
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
self.assertEqual(expected_values[gle.account][1], gle.debit)
|
||||||
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
self.assertEqual(expected_values[gle.account][2], gle.credit)
|
||||||
|
|
||||||
set_perpetual_inventory(0)
|
|
||||||
|
|
||||||
def _insert_purchase_receipt(self):
|
def _insert_purchase_receipt(self):
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import test_records \
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import test_records \
|
||||||
@ -1106,7 +1091,6 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(si.grand_total, 859.43)
|
self.assertEqual(si.grand_total, 859.43)
|
||||||
|
|
||||||
def test_multi_currency_gle(self):
|
def test_multi_currency_gle(self):
|
||||||
set_perpetual_inventory(0)
|
|
||||||
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
|
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
|
||||||
currency="USD", conversion_rate=50)
|
currency="USD", conversion_rate=50)
|
||||||
|
|
||||||
@ -1776,153 +1760,72 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
|
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
|
||||||
|
target_doc.items[0].update({
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC1",
|
||||||
|
"cost_center": "Main - _TC1",
|
||||||
|
"warehouse": "Stores - _TC1"
|
||||||
|
})
|
||||||
target_doc.submit()
|
target_doc.submit()
|
||||||
|
|
||||||
self.assertEqual(target_doc.company, "_Test Company 1")
|
self.assertEqual(target_doc.company, "_Test Company 1")
|
||||||
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
|
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
|
||||||
|
|
||||||
def test_internal_transfer_gl_entry(self):
|
# def test_internal_transfer_gl_entry(self):
|
||||||
## Create internal transfer account
|
# ## Create internal transfer account
|
||||||
account = create_account(account_name="Unrealized Profit",
|
# account = create_account(account_name="Unrealized Profit",
|
||||||
parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
|
# parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
|
||||||
|
|
||||||
frappe.db.set_value('Company', '_Test Company with perpetual inventory',
|
# frappe.db.set_value('Company', '_Test Company with perpetual inventory',
|
||||||
'unrealized_profit_loss_account', account)
|
# 'unrealized_profit_loss_account', account)
|
||||||
|
|
||||||
customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
|
# customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
|
||||||
"_Test Company with perpetual inventory")
|
# "_Test Company with perpetual inventory")
|
||||||
|
|
||||||
create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
|
# create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
|
||||||
"_Test Company with perpetual inventory")
|
# "_Test Company with perpetual inventory")
|
||||||
|
|
||||||
si = create_sales_invoice(
|
# si = create_sales_invoice(
|
||||||
company = "_Test Company with perpetual inventory",
|
# company = "_Test Company with perpetual inventory",
|
||||||
customer = customer,
|
# customer = customer,
|
||||||
debit_to = "Debtors - TCP1",
|
# debit_to = "Debtors - TCP1",
|
||||||
warehouse = "Stores - TCP1",
|
# warehouse = "Stores - TCP1",
|
||||||
income_account = "Sales - TCP1",
|
# income_account = "Sales - TCP1",
|
||||||
expense_account = "Cost of Goods Sold - TCP1",
|
# expense_account = "Cost of Goods Sold - TCP1",
|
||||||
cost_center = "Main - TCP1",
|
# cost_center = "Main - TCP1",
|
||||||
currency = "INR",
|
# currency = "INR",
|
||||||
do_not_save = 1
|
# do_not_save = 1
|
||||||
)
|
# )
|
||||||
|
|
||||||
si.selling_price_list = "_Test Price List Rest of the World"
|
# si.selling_price_list = "_Test Price List Rest of the World"
|
||||||
si.update_stock = 1
|
# si.update_stock = 1
|
||||||
si.items[0].target_warehouse = 'Work In Progress - TCP1'
|
# si.items[0].target_warehouse = 'Work In Progress - TCP1'
|
||||||
add_taxes(si)
|
# add_taxes(si)
|
||||||
si.save()
|
# si.save()
|
||||||
si.submit()
|
# si.submit()
|
||||||
|
|
||||||
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
|
# target_doc = make_inter_company_transaction("Sales Invoice", si.name)
|
||||||
target_doc.company = '_Test Company with perpetual inventory'
|
# target_doc.company = '_Test Company with perpetual inventory'
|
||||||
target_doc.items[0].warehouse = 'Finished Goods - TCP1'
|
# target_doc.items[0].warehouse = 'Finished Goods - TCP1'
|
||||||
add_taxes(target_doc)
|
# add_taxes(target_doc)
|
||||||
target_doc.save()
|
# target_doc.save()
|
||||||
target_doc.submit()
|
# target_doc.submit()
|
||||||
|
|
||||||
si_gl_entries = [
|
# si_gl_entries = [
|
||||||
["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
|
# ["_Test Account Excise Duty - TCP1", 0.0, 12.0, nowdate()],
|
||||||
["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
|
# ["Unrealized Profit - TCP1", 12.0, 0.0, nowdate()]
|
||||||
]
|
# ]
|
||||||
|
|
||||||
check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
|
# check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
|
||||||
|
|
||||||
pi_gl_entries = [
|
# pi_gl_entries = [
|
||||||
["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
|
# ["_Test Account Excise Duty - TCP1", 12.0 , 0.0, nowdate()],
|
||||||
["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
|
# ["Unrealized Profit - TCP1", 0.0, 12.0, nowdate()]
|
||||||
]
|
# ]
|
||||||
|
|
||||||
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
# check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
||||||
|
|
||||||
def test_eway_bill_json(self):
|
def test_eway_bill_json(self):
|
||||||
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
|
si = make_sales_invoice_for_ewaybill()
|
||||||
address = frappe.get_doc({
|
|
||||||
"address_line1": "_Test Address Line 1",
|
|
||||||
"address_title": "_Test Address for Eway bill",
|
|
||||||
"address_type": "Billing",
|
|
||||||
"city": "_Test City",
|
|
||||||
"state": "Test State",
|
|
||||||
"country": "India",
|
|
||||||
"doctype": "Address",
|
|
||||||
"is_primary_address": 1,
|
|
||||||
"phone": "+91 0000000000",
|
|
||||||
"gstin": "27AAECE4835E1ZR",
|
|
||||||
"gst_state": "Maharashtra",
|
|
||||||
"gst_state_number": "27",
|
|
||||||
"pincode": "401108"
|
|
||||||
}).insert()
|
|
||||||
|
|
||||||
address.append("links", {
|
|
||||||
"link_doctype": "Company",
|
|
||||||
"link_name": "_Test Company"
|
|
||||||
})
|
|
||||||
|
|
||||||
address.save()
|
|
||||||
|
|
||||||
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
|
|
||||||
address = frappe.get_doc({
|
|
||||||
"address_line1": "_Test Address Line 1",
|
|
||||||
"address_title": "_Test Customer-Address for Eway bill",
|
|
||||||
"address_type": "Shipping",
|
|
||||||
"city": "_Test City",
|
|
||||||
"state": "Test State",
|
|
||||||
"country": "India",
|
|
||||||
"doctype": "Address",
|
|
||||||
"is_primary_address": 1,
|
|
||||||
"phone": "+91 0000000000",
|
|
||||||
"gst_state": "Maharashtra",
|
|
||||||
"gst_state_number": "27",
|
|
||||||
"pincode": "410038"
|
|
||||||
}).insert()
|
|
||||||
|
|
||||||
address.append("links", {
|
|
||||||
"link_doctype": "Customer",
|
|
||||||
"link_name": "_Test Customer"
|
|
||||||
})
|
|
||||||
|
|
||||||
address.save()
|
|
||||||
|
|
||||||
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"})
|
|
||||||
|
|
||||||
if not gst_account:
|
|
||||||
gst_settings.append("gst_accounts", {
|
|
||||||
"company": "_Test Company",
|
|
||||||
"cgst_account": "CGST - _TC",
|
|
||||||
"sgst_account": "SGST - _TC",
|
|
||||||
"igst_account": "IGST - _TC",
|
|
||||||
})
|
|
||||||
|
|
||||||
gst_settings.save()
|
|
||||||
|
|
||||||
si = create_sales_invoice(do_not_save =1, rate = '60000')
|
|
||||||
|
|
||||||
si.distance = 2000
|
|
||||||
si.company_address = "_Test Address for Eway bill-Billing"
|
|
||||||
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
|
|
||||||
si.vehicle_no = "KA12KA1234"
|
|
||||||
si.gst_category = "Registered Regular"
|
|
||||||
|
|
||||||
si.append("taxes", {
|
|
||||||
"charge_type": "On Net Total",
|
|
||||||
"account_head": "CGST - _TC",
|
|
||||||
"cost_center": "Main - _TC",
|
|
||||||
"description": "CGST @ 9.0",
|
|
||||||
"rate": 9
|
|
||||||
})
|
|
||||||
|
|
||||||
si.append("taxes", {
|
|
||||||
"charge_type": "On Net Total",
|
|
||||||
"account_head": "SGST - _TC",
|
|
||||||
"cost_center": "Main - _TC",
|
|
||||||
"description": "SGST @ 9.0",
|
|
||||||
"rate": 9
|
|
||||||
})
|
|
||||||
|
|
||||||
si.submit()
|
si.submit()
|
||||||
|
|
||||||
@ -1939,6 +1842,214 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
|
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
|
||||||
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
|
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
|
||||||
|
|
||||||
|
def test_einvoice_submission_without_irn(self):
|
||||||
|
# init
|
||||||
|
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
|
||||||
|
country = frappe.flags.country
|
||||||
|
frappe.flags.country = 'India'
|
||||||
|
|
||||||
|
si = make_sales_invoice_for_ewaybill()
|
||||||
|
self.assertRaises(frappe.ValidationError, si.submit)
|
||||||
|
|
||||||
|
si.irn = 'test_irn'
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
# reset
|
||||||
|
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
|
||||||
|
frappe.flags.country = country
|
||||||
|
|
||||||
|
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 = []
|
||||||
|
si.append("items", {
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"uom": "Nos",
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"qty": 2000,
|
||||||
|
"rate": 12,
|
||||||
|
"income_account": "Sales - _TC",
|
||||||
|
"expense_account": "Cost of Goods Sold - _TC",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
})
|
||||||
|
si.append("items", {
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"uom": "Nos",
|
||||||
|
"warehouse": "_Test Warehouse - _TC",
|
||||||
|
"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 = 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(value_details['AssVal'], total_item_ass_value)
|
||||||
|
self.assertEqual(value_details['CgstVal'], total_item_cgst_value)
|
||||||
|
self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
|
||||||
|
self.assertEqual(value_details['IgstVal'], total_item_igst_value)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
value_details['TotInvVal'],
|
||||||
|
value_details['AssVal'] + value_details['CgstVal']
|
||||||
|
+ value_details['SgstVal'] + value_details['IgstVal']
|
||||||
|
+ value_details['OthChrg'] - value_details['Discount']
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
|
||||||
|
self.assertTrue(einvoice['EwbDtls'])
|
||||||
|
|
||||||
|
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",
|
||||||
|
"address_title": "_Test Address for Eway bill",
|
||||||
|
"address_type": "Billing",
|
||||||
|
"city": "_Test City",
|
||||||
|
"state": "Test State",
|
||||||
|
"country": "India",
|
||||||
|
"doctype": "Address",
|
||||||
|
"is_primary_address": 1,
|
||||||
|
"phone": "+910000000000",
|
||||||
|
"gstin": "27AAECE4835E1ZR",
|
||||||
|
"gst_state": "Maharashtra",
|
||||||
|
"gst_state_number": "27",
|
||||||
|
"pincode": "401108"
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
address.append("links", {
|
||||||
|
"link_doctype": "Company",
|
||||||
|
"link_name": "_Test Company"
|
||||||
|
})
|
||||||
|
|
||||||
|
address.save()
|
||||||
|
|
||||||
|
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
|
||||||
|
address = frappe.get_doc({
|
||||||
|
"address_line1": "_Test Address Line 1",
|
||||||
|
"address_title": "_Test Customer-Address for Eway bill",
|
||||||
|
"address_type": "Shipping",
|
||||||
|
"city": "_Test City",
|
||||||
|
"state": "Test State",
|
||||||
|
"country": "India",
|
||||||
|
"doctype": "Address",
|
||||||
|
"is_primary_address": 1,
|
||||||
|
"phone": "+910000000000",
|
||||||
|
"gstin": "27AACCM7806M1Z3",
|
||||||
|
"gst_state": "Maharashtra",
|
||||||
|
"gst_state_number": "27",
|
||||||
|
"pincode": "410038"
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
address.append("links", {
|
||||||
|
"link_doctype": "Customer",
|
||||||
|
"link_name": "_Test Customer"
|
||||||
|
})
|
||||||
|
|
||||||
|
address.save()
|
||||||
|
|
||||||
|
def make_test_transporter_for_ewaybill():
|
||||||
|
if not frappe.db.exists('Supplier', '_Test Transporter'):
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "Supplier",
|
||||||
|
"supplier_name": "_Test Transporter",
|
||||||
|
"country": "India",
|
||||||
|
"supplier_group": "_Test Supplier Group",
|
||||||
|
"supplier_type": "Company",
|
||||||
|
"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"}
|
||||||
|
)
|
||||||
|
|
||||||
|
if not gst_account:
|
||||||
|
gst_settings.append("gst_accounts", {
|
||||||
|
"company": "_Test Company",
|
||||||
|
"cgst_account": "CGST - _TC",
|
||||||
|
"sgst_account": "SGST - _TC",
|
||||||
|
"igst_account": "IGST - _TC",
|
||||||
|
})
|
||||||
|
|
||||||
|
gst_settings.save()
|
||||||
|
|
||||||
|
si = create_sales_invoice(do_not_save=1, rate='60000')
|
||||||
|
|
||||||
|
si.distance = 2000
|
||||||
|
si.company_address = "_Test Address for Eway bill-Billing"
|
||||||
|
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
|
||||||
|
si.vehicle_no = "KA12KA1234"
|
||||||
|
si.gst_category = "Registered Regular"
|
||||||
|
si.mode_of_transport = 'Road'
|
||||||
|
si.transporter = '_Test Transporter'
|
||||||
|
|
||||||
|
si.append("taxes", {
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "CGST - _TC",
|
||||||
|
"cost_center": "Main - _TC",
|
||||||
|
"description": "CGST @ 9.0",
|
||||||
|
"rate": 9
|
||||||
|
})
|
||||||
|
|
||||||
|
si.append("taxes", {
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"account_head": "SGST - _TC",
|
||||||
|
"cost_center": "Main - _TC",
|
||||||
|
"description": "SGST @ 9.0",
|
||||||
|
"rate": 9
|
||||||
|
})
|
||||||
|
|
||||||
|
return si
|
||||||
|
|
||||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||||
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
@ -1991,14 +2102,19 @@ def create_sales_invoice(**args):
|
|||||||
|
|
||||||
si.append("items", {
|
si.append("items", {
|
||||||
"item_code": args.item or args.item_code or "_Test Item",
|
"item_code": args.item or args.item_code or "_Test Item",
|
||||||
|
"item_name": args.item_name or "_Test Item",
|
||||||
|
"description": args.description or "_Test Item",
|
||||||
"gst_hsn_code": "999800",
|
"gst_hsn_code": "999800",
|
||||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||||
"qty": args.qty or 1,
|
"qty": args.qty or 1,
|
||||||
|
"uom": args.uom or "Nos",
|
||||||
|
"stock_uom": args.uom or "Nos",
|
||||||
"rate": args.rate if args.get("rate") is not None else 100,
|
"rate": args.rate if args.get("rate") is not None else 100,
|
||||||
"income_account": args.income_account or "Sales - _TC",
|
"income_account": args.income_account or "Sales - _TC",
|
||||||
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
|
||||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||||
"serial_no": args.serial_no
|
"serial_no": args.serial_no,
|
||||||
|
"conversion_factor": 1
|
||||||
})
|
})
|
||||||
|
|
||||||
if not args.do_not_save:
|
if not args.do_not_save:
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"autoname": "hash",
|
"autoname": "hash",
|
||||||
"creation": "2013-06-04 11:02:19",
|
"creation": "2013-06-04 11:02:19",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
@ -51,6 +52,7 @@
|
|||||||
"column_break_24",
|
"column_break_24",
|
||||||
"base_net_rate",
|
"base_net_rate",
|
||||||
"base_net_amount",
|
"base_net_amount",
|
||||||
|
"incoming_rate",
|
||||||
"drop_ship",
|
"drop_ship",
|
||||||
"delivered_by_supplier",
|
"delivered_by_supplier",
|
||||||
"accounting",
|
"accounting",
|
||||||
@ -792,20 +794,28 @@
|
|||||||
"options": "Project"
|
"options": "Project"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:parent.update_stock == 1",
|
"depends_on": "eval:parent.update_stock == 1",
|
||||||
"fieldname": "sales_invoice_item",
|
"fieldname": "sales_invoice_item",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"ignore_user_permissions": 1,
|
"ignore_user_permissions": 1,
|
||||||
"label": "Sales Invoice Item",
|
"label": "Sales Invoice Item",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "incoming_rate",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Incoming Rate",
|
||||||
|
"no_copy": 1,
|
||||||
|
"print_hide": 1,
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
],
|
],
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-08-20 11:24:41.749986",
|
"modified": "2020-09-23 19:59:04.879322",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
@ -5,23 +5,19 @@ from __future__ import unicode_literals
|
|||||||
import frappe, erpnext
|
import frappe, erpnext
|
||||||
from frappe.utils import flt, cstr, cint, comma_and, today, getdate, formatdate, now
|
from frappe.utils import flt, cstr, cint, comma_and, today, getdate, formatdate, now
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from erpnext.accounts.utils import get_stock_and_account_balance
|
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
|
||||||
|
|
||||||
|
|
||||||
class ClosedAccountingPeriod(frappe.ValidationError): pass
|
class ClosedAccountingPeriod(frappe.ValidationError): pass
|
||||||
class StockAccountInvalidTransaction(frappe.ValidationError): pass
|
|
||||||
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
|
|
||||||
|
|
||||||
def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes'):
|
def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False):
|
||||||
if gl_map:
|
if gl_map:
|
||||||
if not cancel:
|
if not cancel:
|
||||||
validate_accounting_period(gl_map)
|
validate_accounting_period(gl_map)
|
||||||
gl_map = process_gl_map(gl_map, merge_entries)
|
gl_map = process_gl_map(gl_map, merge_entries)
|
||||||
if gl_map and len(gl_map) > 1:
|
if gl_map and len(gl_map) > 1:
|
||||||
save_entries(gl_map, adv_adj, update_outstanding)
|
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||||
else:
|
else:
|
||||||
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
|
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
|
||||||
else:
|
else:
|
||||||
@ -119,8 +115,9 @@ def check_if_in_list(gle, gl_map, dimensions=None):
|
|||||||
if same_head:
|
if same_head:
|
||||||
return e
|
return e
|
||||||
|
|
||||||
def save_entries(gl_map, adv_adj, update_outstanding):
|
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
|
||||||
validate_cwip_accounts(gl_map)
|
if not from_repost:
|
||||||
|
validate_cwip_accounts(gl_map)
|
||||||
|
|
||||||
round_off_debit_credit(gl_map)
|
round_off_debit_credit(gl_map)
|
||||||
|
|
||||||
@ -128,76 +125,19 @@ def save_entries(gl_map, adv_adj, update_outstanding):
|
|||||||
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
|
||||||
|
|
||||||
for entry in gl_map:
|
for entry in gl_map:
|
||||||
make_entry(entry, adv_adj, update_outstanding)
|
make_entry(entry, adv_adj, update_outstanding, from_repost)
|
||||||
|
|
||||||
# check against budget
|
def make_entry(args, adv_adj, update_outstanding, from_repost=False):
|
||||||
validate_expense_against_budget(entry)
|
|
||||||
|
|
||||||
validate_account_for_perpetual_inventory(gl_map)
|
|
||||||
|
|
||||||
|
|
||||||
def make_entry(args, adv_adj, update_outstanding):
|
|
||||||
gle = frappe.new_doc("GL Entry")
|
gle = frappe.new_doc("GL Entry")
|
||||||
gle.update(args)
|
gle.update(args)
|
||||||
gle.flags.ignore_permissions = 1
|
gle.flags.ignore_permissions = 1
|
||||||
|
gle.flags.from_repost = from_repost
|
||||||
gle.insert()
|
gle.insert()
|
||||||
gle.run_method("on_update_with_args", adv_adj, update_outstanding)
|
gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
|
||||||
gle.submit()
|
gle.submit()
|
||||||
|
|
||||||
# check against budget
|
if not from_repost:
|
||||||
validate_expense_against_budget(args)
|
validate_expense_against_budget(args)
|
||||||
|
|
||||||
def validate_account_for_perpetual_inventory(gl_map):
|
|
||||||
if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)):
|
|
||||||
account_list = [gl_entries.account for gl_entries in gl_map]
|
|
||||||
|
|
||||||
aii_accounts = [d.name for d in frappe.get_all("Account",
|
|
||||||
filters={'account_type': 'Stock', 'is_group': 0, 'company': gl_map[0].company})]
|
|
||||||
|
|
||||||
for account in account_list:
|
|
||||||
if account not in aii_accounts:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Always use current date to get stock and account balance as there can future entries for
|
|
||||||
# other items
|
|
||||||
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
|
|
||||||
getdate(), gl_map[0].company)
|
|
||||||
|
|
||||||
if gl_map[0].voucher_type=="Journal Entry":
|
|
||||||
# In case of Journal Entry, there are no corresponding SL entries,
|
|
||||||
# hence deducting currency amount
|
|
||||||
account_bal -= flt(gl_map[0].debit) - flt(gl_map[0].credit)
|
|
||||||
if account_bal == stock_bal:
|
|
||||||
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
|
||||||
.format(account), StockAccountInvalidTransaction)
|
|
||||||
|
|
||||||
elif abs(account_bal - stock_bal) > 0.1:
|
|
||||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
|
|
||||||
currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
|
|
||||||
|
|
||||||
diff = flt(stock_bal - account_bal, precision)
|
|
||||||
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format(
|
|
||||||
stock_bal, account_bal, frappe.bold(account))
|
|
||||||
error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
|
|
||||||
stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
|
|
||||||
|
|
||||||
db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency')
|
|
||||||
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency')
|
|
||||||
|
|
||||||
journal_entry_args = {
|
|
||||||
'accounts':[
|
|
||||||
{'account': account, db_or_cr_warehouse_account : abs(diff)},
|
|
||||||
{'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }]
|
|
||||||
}
|
|
||||||
|
|
||||||
frappe.msgprint(msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
|
|
||||||
raise_exception=StockValueAndAccountBalanceOutOfSync,
|
|
||||||
title=_('Values Out Of Sync'),
|
|
||||||
primary_action={
|
|
||||||
'label': _('Make Journal Entry'),
|
|
||||||
'client_action': 'erpnext.route_to_adjustment_jv',
|
|
||||||
'args': journal_entry_args
|
|
||||||
})
|
|
||||||
|
|
||||||
def validate_cwip_accounts(gl_map):
|
def validate_cwip_accounts(gl_map):
|
||||||
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
|
cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
|
||||||
|
162
erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
Normal file
162
erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
|
||||||
|
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
|
||||||
|
|
||||||
|
<div class="page-break">
|
||||||
|
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||||
|
{% if letter_head and not no_letterhead %}
|
||||||
|
<div class="letter-head">{{ letter_head }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="print-heading">
|
||||||
|
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if print_settings.repeat_header_footer %}
|
||||||
|
<div id="footer-html" class="visible-pdf">
|
||||||
|
{% if not no_letterhead and footer %}
|
||||||
|
<div class="letter-head-footer">
|
||||||
|
{{ footer }}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<p class="text-center small page-number visible-pdf">
|
||||||
|
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
|
||||||
|
<h5 class="font-bold" style="margin-left: 15px; margin-top: 0px;">1. Transaction Details</h5>
|
||||||
|
<div class="col-xs-8 column-break">
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>IRN</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Ack. No</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Ack. Date</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Category</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Document Type</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="row data-field">
|
||||||
|
<div class="col-xs-4"><label>Document No</label></div>
|
||||||
|
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4 column-break">
|
||||||
|
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
|
||||||
|
<h5 class="font-bold" style="margin-left: 15px; margin-bottom: 0px;">2. Party Details</h5>
|
||||||
|
{%- set seller = einvoice.SellerDtls -%}
|
||||||
|
<div class="col-xs-6 column-break">
|
||||||
|
<h5 style="margin-bottom: 5px;">Seller</h5>
|
||||||
|
<p>{{ seller.Gstin }}</p>
|
||||||
|
<p>{{ seller.LglNm }}</p>
|
||||||
|
<p>{{ seller.Addr1 }}</p>
|
||||||
|
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
|
||||||
|
<p>{{ seller.Loc }}</p>
|
||||||
|
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
|
||||||
|
|
||||||
|
{%- if einvoice.ShipDtls -%}
|
||||||
|
{%- set shipping = einvoice.ShipDtls -%}
|
||||||
|
<h5 style="margin-bottom: 5px;">Shipping</h5>
|
||||||
|
<p>{{ shipping.Gstin }}</p>
|
||||||
|
<p>{{ shipping.LglNm }}</p>
|
||||||
|
<p>{{ shipping.Addr1 }}</p>
|
||||||
|
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
|
||||||
|
<p>{{ shipping.Loc }}</p>
|
||||||
|
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{%- set buyer = einvoice.BuyerDtls -%}
|
||||||
|
<div class="col-xs-6 column-break">
|
||||||
|
<h5 style="margin-bottom: 5px;">Buyer</h5>
|
||||||
|
<p>{{ buyer.Gstin }}</p>
|
||||||
|
<p>{{ buyer.LglNm }}</p>
|
||||||
|
<p>{{ buyer.Addr1 }}</p>
|
||||||
|
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
|
||||||
|
<p>{{ buyer.Loc }}</p>
|
||||||
|
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<h5 class="font-bold" style="margin-bottom: 0px;">3. Item Details</h5>
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left" style="width: 3%;">Sr. No.</th>
|
||||||
|
<th class="text-left">Item</th>
|
||||||
|
<th class="text-left" style="width: 10%;">HSN Code</th>
|
||||||
|
<th class="text-left" style="width: 5%;">Qty</th>
|
||||||
|
<th class="text-left" style="width: 5%;">UOM</th>
|
||||||
|
<th class="text-left">Rate</th>
|
||||||
|
<th class="text-left" style="width: 5%;">Discount</th>
|
||||||
|
<th class="text-left">Taxable Amount</th>
|
||||||
|
<th class="text-left" style="width: 7%;">Tax Rate</th>
|
||||||
|
<th class="text-left" style="width: 5%;">Other Charges</th>
|
||||||
|
<th class="text-left">Total</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for item in einvoice.ItemList %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
|
||||||
|
<td class="text-left">{{ item.PrdDesc }}</td>
|
||||||
|
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
|
||||||
|
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
|
||||||
|
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
|
||||||
|
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
|
||||||
|
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
|
||||||
|
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<div style="overflow-x: auto;">
|
||||||
|
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
|
||||||
|
<table class="table table-bordered">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="text-left">Taxable Amount</th>
|
||||||
|
<th class="text-left">CGST</th>
|
||||||
|
<th class="text-left"">SGST</th>
|
||||||
|
<th class="text-left">IGST</th>
|
||||||
|
<th class="text-left">CESS</th>
|
||||||
|
<th class="text-left" style="width: 10%;">State CESS</th>
|
||||||
|
<th class="text-left">Discount</th>
|
||||||
|
<th class="text-left" style="width: 10%;">Other Charges</th>
|
||||||
|
<th class="text-left" style="width: 10%;">Round Off</th>
|
||||||
|
<th class="text-left">Total Value</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{%- set value_details = einvoice.ValDtls -%}
|
||||||
|
<tr>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
|
||||||
|
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"align_labels_right": 1,
|
||||||
|
"creation": "2020-10-10 18:01:21.032914",
|
||||||
|
"custom_format": 0,
|
||||||
|
"default_print_language": "en-US",
|
||||||
|
"disabled": 1,
|
||||||
|
"doc_type": "Sales Invoice",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Print Format",
|
||||||
|
"font": "Default",
|
||||||
|
"html": "",
|
||||||
|
"idx": 0,
|
||||||
|
"line_breaks": 1,
|
||||||
|
"modified": "2020-10-23 19:54:40.634936",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "GST E-Invoice",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"print_format_builder": 0,
|
||||||
|
"print_format_type": "Jinja",
|
||||||
|
"raw_printing": 0,
|
||||||
|
"show_section_headings": 1,
|
||||||
|
"standard": "Yes"
|
||||||
|
}
|
@ -47,21 +47,22 @@ def get_data(filters):
|
|||||||
|
|
||||||
for d in gl_entries:
|
for d in gl_entries:
|
||||||
asset_data = assets_details.get(d.against_voucher)
|
asset_data = assets_details.get(d.against_voucher)
|
||||||
if not asset_data.get("accumulated_depreciation_amount"):
|
if asset_data:
|
||||||
asset_data.accumulated_depreciation_amount = d.debit
|
if not asset_data.get("accumulated_depreciation_amount"):
|
||||||
else:
|
asset_data.accumulated_depreciation_amount = d.debit
|
||||||
asset_data.accumulated_depreciation_amount += d.debit
|
else:
|
||||||
|
asset_data.accumulated_depreciation_amount += d.debit
|
||||||
|
|
||||||
row = frappe._dict(asset_data)
|
row = frappe._dict(asset_data)
|
||||||
row.update({
|
row.update({
|
||||||
"depreciation_amount": d.debit,
|
"depreciation_amount": d.debit,
|
||||||
"depreciation_date": d.posting_date,
|
"depreciation_date": d.posting_date,
|
||||||
"amount_after_depreciation": (flt(row.gross_purchase_amount) -
|
"amount_after_depreciation": (flt(row.gross_purchase_amount) -
|
||||||
flt(row.accumulated_depreciation_amount)),
|
flt(row.accumulated_depreciation_amount)),
|
||||||
"depreciation_entry": d.voucher_no
|
"depreciation_entry": d.voucher_no
|
||||||
})
|
})
|
||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
@ -12,11 +12,12 @@ from frappe.utils import formatdate, get_number_format_info
|
|||||||
from six import iteritems
|
from six import iteritems
|
||||||
# imported to enable erpnext.accounts.utils.get_account_currency
|
# imported to enable erpnext.accounts.utils.get_account_currency
|
||||||
from erpnext.accounts.doctype.account.account import get_account_currency
|
from erpnext.accounts.doctype.account.account import get_account_currency
|
||||||
|
from frappe.model.meta import get_field_precision
|
||||||
|
|
||||||
from erpnext.stock.utils import get_stock_value_on
|
from erpnext.stock.utils import get_stock_value_on
|
||||||
from erpnext.stock import get_warehouse_account_map
|
from erpnext.stock import get_warehouse_account_map
|
||||||
|
|
||||||
|
class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
|
||||||
class FiscalYearError(frappe.ValidationError): pass
|
class FiscalYearError(frappe.ValidationError): pass
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@ -585,24 +586,6 @@ def fix_total_debit_credit():
|
|||||||
(dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr),
|
(dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr),
|
||||||
(d.diff, d.voucher_type, d.voucher_no))
|
(d.diff, d.voucher_type, d.voucher_no))
|
||||||
|
|
||||||
def get_stock_and_account_balance(account=None, posting_date=None, company=None):
|
|
||||||
if not posting_date: posting_date = nowdate()
|
|
||||||
|
|
||||||
warehouse_account = get_warehouse_account_map(company)
|
|
||||||
|
|
||||||
account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True)
|
|
||||||
|
|
||||||
related_warehouses = [wh for wh, wh_details in warehouse_account.items()
|
|
||||||
if wh_details.account == account and not wh_details.is_group]
|
|
||||||
|
|
||||||
total_stock_value = 0.0
|
|
||||||
for warehouse in related_warehouses:
|
|
||||||
value = get_stock_value_on(warehouse, posting_date)
|
|
||||||
total_stock_value += value
|
|
||||||
|
|
||||||
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
|
|
||||||
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
|
|
||||||
|
|
||||||
def get_currency_precision():
|
def get_currency_precision():
|
||||||
precision = cint(frappe.db.get_default("currency_precision"))
|
precision = cint(frappe.db.get_default("currency_precision"))
|
||||||
if not precision:
|
if not precision:
|
||||||
@ -903,12 +886,6 @@ def get_coa(doctype, parent, is_root, chart=None):
|
|||||||
|
|
||||||
return accounts
|
return accounts
|
||||||
|
|
||||||
def get_stock_accounts(company):
|
|
||||||
return frappe.get_all("Account", filters = {
|
|
||||||
"account_type": "Stock",
|
|
||||||
"company": company
|
|
||||||
})
|
|
||||||
|
|
||||||
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
|
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
|
||||||
warehouse_account=None, company=None):
|
warehouse_account=None, company=None):
|
||||||
def _delete_gl_entries(voucher_type, voucher_no):
|
def _delete_gl_entries(voucher_type, voucher_no):
|
||||||
@ -928,7 +905,7 @@ def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for
|
|||||||
if expected_gle:
|
if expected_gle:
|
||||||
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
|
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
|
||||||
_delete_gl_entries(voucher_type, voucher_no)
|
_delete_gl_entries(voucher_type, voucher_no)
|
||||||
voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True)
|
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
|
||||||
else:
|
else:
|
||||||
_delete_gl_entries(voucher_type, voucher_no)
|
_delete_gl_entries(voucher_type, voucher_no)
|
||||||
|
|
||||||
@ -947,7 +924,10 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
|
|||||||
|
|
||||||
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
|
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
|
||||||
from `tabStock Ledger Entry` sle
|
from `tabStock Ledger Entry` sle
|
||||||
where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition}
|
where
|
||||||
|
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
|
||||||
|
and is_cancelled = 0
|
||||||
|
{condition}
|
||||||
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
|
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
|
||||||
tuple([posting_date, posting_time] + values), as_dict=True):
|
tuple([posting_date, posting_time] + values), as_dict=True):
|
||||||
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
|
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
|
||||||
@ -964,3 +944,106 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
|
|||||||
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
|
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
|
||||||
|
|
||||||
return gl_entries
|
return gl_entries
|
||||||
|
|
||||||
|
def compare_existing_and_expected_gle(existing_gle, expected_gle):
|
||||||
|
matched = True
|
||||||
|
for entry in expected_gle:
|
||||||
|
account_existed = False
|
||||||
|
for e in existing_gle:
|
||||||
|
if entry.account == e.account:
|
||||||
|
account_existed = True
|
||||||
|
if entry.account == e.account and entry.against_account == e.against_account \
|
||||||
|
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
|
||||||
|
and (entry.debit != e.debit or entry.credit != e.credit):
|
||||||
|
matched = False
|
||||||
|
break
|
||||||
|
if not account_existed:
|
||||||
|
matched = False
|
||||||
|
break
|
||||||
|
return matched
|
||||||
|
|
||||||
|
def check_if_stock_and_account_balance_synced(posting_date, company, voucher_type=None, voucher_no=None):
|
||||||
|
if not cint(erpnext.is_perpetual_inventory_enabled(company)):
|
||||||
|
return
|
||||||
|
|
||||||
|
accounts = get_stock_accounts(company, voucher_type, voucher_no)
|
||||||
|
stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account")
|
||||||
|
|
||||||
|
for account in accounts:
|
||||||
|
account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
|
||||||
|
posting_date, company)
|
||||||
|
|
||||||
|
if abs(account_bal - stock_bal) > 0.1:
|
||||||
|
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
|
||||||
|
currency=frappe.get_cached_value('Company', company, "default_currency"))
|
||||||
|
|
||||||
|
diff = flt(stock_bal - account_bal, precision)
|
||||||
|
|
||||||
|
error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format(
|
||||||
|
stock_bal, account_bal, frappe.bold(account), posting_date)
|
||||||
|
error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\
|
||||||
|
.format(frappe.bold(diff), frappe.bold(posting_date))
|
||||||
|
|
||||||
|
frappe.msgprint(
|
||||||
|
msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
|
||||||
|
raise_exception=StockValueAndAccountBalanceOutOfSync,
|
||||||
|
title=_('Values Out Of Sync'),
|
||||||
|
primary_action={
|
||||||
|
'label': _('Make Journal Entry'),
|
||||||
|
'client_action': 'erpnext.route_to_adjustment_jv',
|
||||||
|
'args': get_journal_entry(account, stock_adjustment_account, diff)
|
||||||
|
})
|
||||||
|
|
||||||
|
def get_stock_accounts(company, voucher_type=None, voucher_no=None):
|
||||||
|
stock_accounts = [d.name for d in frappe.db.get_all("Account", {
|
||||||
|
"account_type": "Stock",
|
||||||
|
"company": company,
|
||||||
|
"is_group": 0
|
||||||
|
})]
|
||||||
|
if voucher_type and voucher_no:
|
||||||
|
if voucher_type == "Journal Entry":
|
||||||
|
stock_accounts = [d.account for d in frappe.db.get_all("Journal Entry Account", {
|
||||||
|
"parent": voucher_no,
|
||||||
|
"account": ["in", stock_accounts]
|
||||||
|
}, "account")]
|
||||||
|
|
||||||
|
else:
|
||||||
|
stock_accounts = [d.account for d in frappe.db.get_all("GL Entry", {
|
||||||
|
"voucher_type": voucher_type,
|
||||||
|
"voucher_no": voucher_no,
|
||||||
|
"account": ["in", stock_accounts]
|
||||||
|
}, "account")]
|
||||||
|
|
||||||
|
return stock_accounts
|
||||||
|
|
||||||
|
def get_stock_and_account_balance(account=None, posting_date=None, company=None):
|
||||||
|
if not posting_date: posting_date = nowdate()
|
||||||
|
|
||||||
|
warehouse_account = get_warehouse_account_map(company)
|
||||||
|
|
||||||
|
account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True)
|
||||||
|
|
||||||
|
related_warehouses = [wh for wh, wh_details in warehouse_account.items()
|
||||||
|
if wh_details.account == account and not wh_details.is_group]
|
||||||
|
|
||||||
|
total_stock_value = 0.0
|
||||||
|
for warehouse in related_warehouses:
|
||||||
|
value = get_stock_value_on(warehouse, posting_date)
|
||||||
|
total_stock_value += value
|
||||||
|
|
||||||
|
precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
|
||||||
|
return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
|
||||||
|
|
||||||
|
def get_journal_entry(account, stock_adjustment_account, amount):
|
||||||
|
db_or_cr_warehouse_account =('credit_in_account_currency' if amount < 0 else 'debit_in_account_currency')
|
||||||
|
db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if amount < 0 else 'credit_in_account_currency')
|
||||||
|
|
||||||
|
return {
|
||||||
|
'accounts':[{
|
||||||
|
'account': account,
|
||||||
|
db_or_cr_warehouse_account: abs(amount)
|
||||||
|
}, {
|
||||||
|
'account': stock_adjustment_account,
|
||||||
|
db_or_cr_stock_adjustment_account : abs(amount)
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
@ -13,17 +13,14 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g
|
|||||||
class AssetValueAdjustment(Document):
|
class AssetValueAdjustment(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
self.validate_date()
|
self.validate_date()
|
||||||
self.set_difference_amount()
|
|
||||||
self.set_current_asset_value()
|
self.set_current_asset_value()
|
||||||
|
self.set_difference_amount()
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.make_depreciation_entry()
|
self.make_depreciation_entry()
|
||||||
self.reschedule_depreciations(self.new_asset_value)
|
self.reschedule_depreciations(self.new_asset_value)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
if self.journal_entry:
|
|
||||||
frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry))
|
|
||||||
|
|
||||||
self.reschedule_depreciations(self.current_asset_value)
|
self.reschedule_depreciations(self.current_asset_value)
|
||||||
|
|
||||||
def validate_date(self):
|
def validate_date(self):
|
||||||
|
@ -732,7 +732,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-30 11:59:47.670951",
|
"modified": "2020-12-07 11:59:47.670951",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
@ -110,9 +110,15 @@ class AccountsController(TransactionBase):
|
|||||||
self.set_inter_company_account()
|
self.set_inter_company_account()
|
||||||
|
|
||||||
validate_regional(self)
|
validate_regional(self)
|
||||||
|
|
||||||
|
validate_einvoice_fields(self)
|
||||||
|
|
||||||
if self.doctype != 'Material Request':
|
if self.doctype != 'Material Request':
|
||||||
apply_pricing_rule_on_transaction(self)
|
apply_pricing_rule_on_transaction(self)
|
||||||
|
|
||||||
|
def before_cancel(self):
|
||||||
|
validate_einvoice_fields(self)
|
||||||
|
|
||||||
def validate_deferred_start_and_end_date(self):
|
def validate_deferred_start_and_end_date(self):
|
||||||
for d in self.items:
|
for d in self.items:
|
||||||
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
|
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
|
||||||
@ -1518,3 +1524,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
@erpnext.allow_regional
|
@erpnext.allow_regional
|
||||||
def validate_regional(doc):
|
def validate_regional(doc):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@erpnext.allow_regional
|
||||||
|
def validate_einvoice_fields(doc):
|
||||||
|
pass
|
||||||
|
@ -16,6 +16,8 @@ from frappe.contacts.doctype.address.address import get_address_display
|
|||||||
|
|
||||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||||
|
from erpnext.stock.utils import get_incoming_rate
|
||||||
|
|
||||||
class BuyingController(StockController):
|
class BuyingController(StockController):
|
||||||
def __setup__(self):
|
def __setup__(self):
|
||||||
@ -63,7 +65,7 @@ class BuyingController(StockController):
|
|||||||
self.set_landed_cost_voucher_amount()
|
self.set_landed_cost_voucher_amount()
|
||||||
|
|
||||||
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||||
self.update_valuation_rate("items")
|
self.update_valuation_rate()
|
||||||
|
|
||||||
def set_missing_values(self, for_validate=False):
|
def set_missing_values(self, for_validate=False):
|
||||||
super(BuyingController, self).set_missing_values(for_validate)
|
super(BuyingController, self).set_missing_values(for_validate)
|
||||||
@ -177,7 +179,7 @@ class BuyingController(StockController):
|
|||||||
self.in_words = money_in_words(amount, self.currency)
|
self.in_words = money_in_words(amount, self.currency)
|
||||||
|
|
||||||
# update valuation rate
|
# update valuation rate
|
||||||
def update_valuation_rate(self, parentfield):
|
def update_valuation_rate(self, reset_outgoing_rate=True):
|
||||||
"""
|
"""
|
||||||
item_tax_amount is the total tax amount applied on that item
|
item_tax_amount is the total tax amount applied on that item
|
||||||
stored for valuation
|
stored for valuation
|
||||||
@ -188,7 +190,7 @@ class BuyingController(StockController):
|
|||||||
|
|
||||||
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
|
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
|
||||||
last_item_idx = 1
|
last_item_idx = 1
|
||||||
for d in self.get(parentfield):
|
for d in self.get("items"):
|
||||||
if d.item_code and d.item_code in stock_and_asset_items:
|
if d.item_code and d.item_code in stock_and_asset_items:
|
||||||
stock_and_asset_items_qty += flt(d.qty)
|
stock_and_asset_items_qty += flt(d.qty)
|
||||||
stock_and_asset_items_amount += flt(d.base_net_amount)
|
stock_and_asset_items_amount += flt(d.base_net_amount)
|
||||||
@ -198,7 +200,7 @@ class BuyingController(StockController):
|
|||||||
if d.category in ["Valuation", "Valuation and Total"]])
|
if d.category in ["Valuation", "Valuation and Total"]])
|
||||||
|
|
||||||
valuation_amount_adjustment = total_valuation_amount
|
valuation_amount_adjustment = total_valuation_amount
|
||||||
for i, item in enumerate(self.get(parentfield)):
|
for i, item in enumerate(self.get("items")):
|
||||||
if item.item_code and item.qty and item.item_code in stock_and_asset_items:
|
if item.item_code and item.qty and item.item_code in stock_and_asset_items:
|
||||||
item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
|
item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
|
||||||
else flt(item.qty) / stock_and_asset_items_qty
|
else flt(item.qty) / stock_and_asset_items_qty
|
||||||
@ -216,16 +218,34 @@ class BuyingController(StockController):
|
|||||||
item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
|
||||||
|
|
||||||
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
|
||||||
rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
|
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
|
||||||
|
item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost
|
||||||
landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \
|
+ flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom)
|
||||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
|
|
||||||
|
|
||||||
item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + rm_supp_cost
|
|
||||||
+ landed_cost_voucher_amount) / qty_in_stock_uom)
|
|
||||||
else:
|
else:
|
||||||
item.valuation_rate = 0.0
|
item.valuation_rate = 0.0
|
||||||
|
|
||||||
|
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
|
||||||
|
supplied_items_cost = 0.0
|
||||||
|
for d in self.get("supplied_items"):
|
||||||
|
if d.reference_name == item_row_id:
|
||||||
|
if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
|
||||||
|
rate = get_incoming_rate({
|
||||||
|
"item_code": d.rm_item_code,
|
||||||
|
"warehouse": self.supplier_warehouse,
|
||||||
|
"posting_date": self.posting_date,
|
||||||
|
"posting_time": self.posting_time,
|
||||||
|
"qty": -1 * d.consumed_qty,
|
||||||
|
"serial_no": d.serial_no
|
||||||
|
})
|
||||||
|
|
||||||
|
if rate > 0:
|
||||||
|
d.rate = rate
|
||||||
|
|
||||||
|
d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount"))
|
||||||
|
supplied_items_cost += flt(d.amount)
|
||||||
|
|
||||||
|
return supplied_items_cost
|
||||||
|
|
||||||
def validate_for_subcontracting(self):
|
def validate_for_subcontracting(self):
|
||||||
if not self.is_subcontracted and self.sub_contracted_items:
|
if not self.is_subcontracted and self.sub_contracted_items:
|
||||||
frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
|
frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
|
||||||
@ -352,35 +372,17 @@ class BuyingController(StockController):
|
|||||||
else:
|
else:
|
||||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||||
|
|
||||||
def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty):
|
def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
|
||||||
rm = self.append('supplied_items', {})
|
rm = self.append('supplied_items', {})
|
||||||
rm.update(raw_material_data)
|
rm.update(raw_material_data)
|
||||||
|
|
||||||
if not rm.main_item_code:
|
if not rm.main_item_code:
|
||||||
rm.main_item_code = fg_item_doc.item_code
|
rm.main_item_code = fg_item_row.item_code
|
||||||
|
|
||||||
rm.reference_name = fg_item_doc.name
|
rm.reference_name = fg_item_row.name
|
||||||
rm.required_qty = qty
|
rm.required_qty = qty
|
||||||
rm.consumed_qty = qty
|
rm.consumed_qty = qty
|
||||||
|
|
||||||
if not raw_material_data.get('non_stock_item'):
|
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
|
||||||
rm.rate = get_incoming_rate({
|
|
||||||
"item_code": raw_material_data.rm_item_code,
|
|
||||||
"warehouse": self.supplier_warehouse,
|
|
||||||
"posting_date": self.posting_date,
|
|
||||||
"posting_time": self.posting_time,
|
|
||||||
"qty": -1 * qty,
|
|
||||||
"serial_no": rm.serial_no
|
|
||||||
})
|
|
||||||
|
|
||||||
if not rm.rate:
|
|
||||||
rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
|
|
||||||
self.doctype, self.name, currency=self.company_currency, company=self.company)
|
|
||||||
|
|
||||||
rm.amount = qty * flt(rm.rate)
|
|
||||||
fg_item_doc.rm_supp_cost += rm.amount
|
|
||||||
|
|
||||||
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
|
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
|
||||||
exploded_item = 1
|
exploded_item = 1
|
||||||
if hasattr(item, 'include_exploded_items'):
|
if hasattr(item, 'include_exploded_items'):
|
||||||
@ -389,7 +391,7 @@ class BuyingController(StockController):
|
|||||||
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
|
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
|
||||||
|
|
||||||
used_alternative_items = []
|
used_alternative_items = []
|
||||||
if self.doctype == 'Purchase Receipt' and item.purchase_order:
|
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
|
||||||
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
|
used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
|
||||||
|
|
||||||
raw_materials_cost = 0
|
raw_materials_cost = 0
|
||||||
@ -406,7 +408,7 @@ class BuyingController(StockController):
|
|||||||
reserve_warehouse = None
|
reserve_warehouse = None
|
||||||
|
|
||||||
conversion_factor = item.conversion_factor
|
conversion_factor = item.conversion_factor
|
||||||
if (self.doctype == 'Purchase Receipt' and item.purchase_order and
|
if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
|
||||||
bom_item.item_code in used_alternative_items):
|
bom_item.item_code in used_alternative_items):
|
||||||
alternative_item_data = used_alternative_items.get(bom_item.item_code)
|
alternative_item_data = used_alternative_items.get(bom_item.item_code)
|
||||||
bom_item.item_code = alternative_item_data.item_code
|
bom_item.item_code = alternative_item_data.item_code
|
||||||
@ -434,9 +436,7 @@ class BuyingController(StockController):
|
|||||||
rm.rm_item_code = bom_item.item_code
|
rm.rm_item_code = bom_item.item_code
|
||||||
rm.stock_uom = bom_item.stock_uom
|
rm.stock_uom = bom_item.stock_uom
|
||||||
rm.required_qty = required_qty
|
rm.required_qty = required_qty
|
||||||
if self.doctype == "Purchase Order" and not rm.reserve_warehouse:
|
rm.rate = bom_item.rate
|
||||||
rm.reserve_warehouse = reserve_warehouse
|
|
||||||
|
|
||||||
rm.conversion_factor = conversion_factor
|
rm.conversion_factor = conversion_factor
|
||||||
|
|
||||||
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
|
||||||
@ -444,29 +444,8 @@ class BuyingController(StockController):
|
|||||||
rm.description = bom_item.description
|
rm.description = bom_item.description
|
||||||
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
|
if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
|
||||||
rm.batch_no = item.batch_no
|
rm.batch_no = item.batch_no
|
||||||
|
elif not rm.reserve_warehouse:
|
||||||
# get raw materials rate
|
rm.reserve_warehouse = reserve_warehouse
|
||||||
if self.doctype == "Purchase Receipt":
|
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
|
||||||
rm.rate = get_incoming_rate({
|
|
||||||
"item_code": bom_item.item_code,
|
|
||||||
"warehouse": self.supplier_warehouse,
|
|
||||||
"posting_date": self.posting_date,
|
|
||||||
"posting_time": self.posting_time,
|
|
||||||
"qty": -1 * required_qty,
|
|
||||||
"serial_no": rm.serial_no
|
|
||||||
})
|
|
||||||
if not rm.rate:
|
|
||||||
rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse,
|
|
||||||
self.doctype, self.name, currency=self.company_currency, company = self.company)
|
|
||||||
else:
|
|
||||||
rm.rate = bom_item.rate
|
|
||||||
|
|
||||||
rm.amount = required_qty * flt(rm.rate)
|
|
||||||
raw_materials_cost += flt(rm.amount)
|
|
||||||
|
|
||||||
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
|
|
||||||
item.rm_supp_cost = raw_materials_cost
|
|
||||||
|
|
||||||
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
|
def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
|
||||||
"""Remove all those child items which are no longer present in main item table"""
|
"""Remove all those child items which are no longer present in main item table"""
|
||||||
@ -579,7 +558,8 @@ class BuyingController(StockController):
|
|||||||
or (cint(self.is_return) and self.docstatus==2)):
|
or (cint(self.is_return) and self.docstatus==2)):
|
||||||
from_warehouse_sle = self.get_sl_entries(d, {
|
from_warehouse_sle = self.get_sl_entries(d, {
|
||||||
"actual_qty": -1 * pr_qty,
|
"actual_qty": -1 * pr_qty,
|
||||||
"warehouse": d.from_warehouse
|
"warehouse": d.from_warehouse,
|
||||||
|
"dependant_sle_voucher_detail_no": d.name
|
||||||
})
|
})
|
||||||
|
|
||||||
sl_entries.append(from_warehouse_sle)
|
sl_entries.append(from_warehouse_sle)
|
||||||
@ -589,28 +569,20 @@ class BuyingController(StockController):
|
|||||||
"serial_no": cstr(d.serial_no).strip()
|
"serial_no": cstr(d.serial_no).strip()
|
||||||
})
|
})
|
||||||
if self.is_return:
|
if self.is_return:
|
||||||
filters = {
|
outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
|
||||||
"voucher_type": self.doctype,
|
|
||||||
"voucher_no": self.return_against,
|
|
||||||
"item_code": d.item_code
|
|
||||||
}
|
|
||||||
|
|
||||||
if (self.doctype == "Purchase Invoice" and self.update_stock
|
|
||||||
and d.get("purchase_invoice_item")):
|
|
||||||
filters["voucher_detail_no"] = d.purchase_invoice_item
|
|
||||||
elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
|
|
||||||
filters["voucher_detail_no"] = d.purchase_receipt_item
|
|
||||||
|
|
||||||
original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
|
|
||||||
|
|
||||||
sle.update({
|
sle.update({
|
||||||
"outgoing_rate": original_incoming_rate
|
"outgoing_rate": outgoing_rate,
|
||||||
|
"recalculate_rate": 1
|
||||||
})
|
})
|
||||||
|
if d.from_warehouse:
|
||||||
|
sle.dependant_sle_voucher_detail_no = d.name
|
||||||
else:
|
else:
|
||||||
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
|
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
|
||||||
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
|
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
|
||||||
sle.update({
|
sle.update({
|
||||||
"incoming_rate": incoming_rate
|
"incoming_rate": incoming_rate,
|
||||||
|
"recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0
|
||||||
})
|
})
|
||||||
sl_entries.append(sle)
|
sl_entries.append(sle)
|
||||||
|
|
||||||
@ -618,7 +590,8 @@ class BuyingController(StockController):
|
|||||||
or (cint(self.is_return) and self.docstatus==1)):
|
or (cint(self.is_return) and self.docstatus==1)):
|
||||||
from_warehouse_sle = self.get_sl_entries(d, {
|
from_warehouse_sle = self.get_sl_entries(d, {
|
||||||
"actual_qty": -1 * pr_qty,
|
"actual_qty": -1 * pr_qty,
|
||||||
"warehouse": d.from_warehouse
|
"warehouse": d.from_warehouse,
|
||||||
|
"recalculate_rate": 1
|
||||||
})
|
})
|
||||||
|
|
||||||
sl_entries.append(from_warehouse_sle)
|
sl_entries.append(from_warehouse_sle)
|
||||||
@ -666,6 +639,7 @@ class BuyingController(StockController):
|
|||||||
"item_code": d.rm_item_code,
|
"item_code": d.rm_item_code,
|
||||||
"warehouse": self.supplier_warehouse,
|
"warehouse": self.supplier_warehouse,
|
||||||
"actual_qty": -1*flt(d.consumed_qty),
|
"actual_qty": -1*flt(d.consumed_qty),
|
||||||
|
"dependant_sle_voucher_detail_no": d.reference_name
|
||||||
}))
|
}))
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
@ -857,6 +831,7 @@ class BuyingController(StockController):
|
|||||||
else:
|
else:
|
||||||
validate_item_type(self, "is_purchase_item", "purchase")
|
validate_item_type(self, "is_purchase_item", "purchase")
|
||||||
|
|
||||||
|
|
||||||
def get_items_from_bom(item_code, bom, exploded_item=1):
|
def get_items_from_bom(item_code, bom, exploded_item=1):
|
||||||
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
|
||||||
|
|
||||||
|
@ -204,21 +204,25 @@ def get_already_returned_items(doc):
|
|||||||
return items
|
return items
|
||||||
|
|
||||||
def get_returned_qty_map_for_row(row_name, doctype):
|
def get_returned_qty_map_for_row(row_name, doctype):
|
||||||
|
if doctype == "POS Invoice": return {}
|
||||||
|
|
||||||
child_doctype = doctype + " Item"
|
child_doctype = doctype + " Item"
|
||||||
reference_field = frappe.scrub(child_doctype) if doctype == "Purchase Receipt" else "dn_detail"
|
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
|
||||||
|
|
||||||
fields = [
|
fields = [
|
||||||
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
|
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
|
||||||
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
|
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
|
||||||
]
|
]
|
||||||
|
|
||||||
if doctype == "Purchase Receipt":
|
if doctype in ("Purchase Receipt", "Purchase Invoice"):
|
||||||
fields += [
|
fields += [
|
||||||
"sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
|
"sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
|
||||||
"sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype),
|
"sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype)
|
||||||
"sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if doctype == "Purchase Receipt":
|
||||||
|
fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
|
||||||
|
|
||||||
data = frappe.db.get_list(doctype,
|
data = frappe.db.get_list(doctype,
|
||||||
fields = fields,
|
fields = fields,
|
||||||
filters = [
|
filters = [
|
||||||
@ -231,6 +235,7 @@ def get_returned_qty_map_for_row(row_name, doctype):
|
|||||||
|
|
||||||
def make_return_doc(doctype, source_name, target_doc=None):
|
def make_return_doc(doctype, source_name, target_doc=None):
|
||||||
from frappe.model.mapper import get_mapped_doc
|
from frappe.model.mapper import get_mapped_doc
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
company = frappe.db.get_value("Delivery Note", source_name, "company")
|
company = frappe.db.get_value("Delivery Note", source_name, "company")
|
||||||
default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return")
|
default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return")
|
||||||
|
|
||||||
@ -290,6 +295,12 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
|||||||
def update_item(source_doc, target_doc, source_parent):
|
def update_item(source_doc, target_doc, source_parent):
|
||||||
target_doc.qty = -1 * source_doc.qty
|
target_doc.qty = -1 * source_doc.qty
|
||||||
|
|
||||||
|
if source_doc.serial_no:
|
||||||
|
returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
|
||||||
|
serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos))
|
||||||
|
if serial_nos:
|
||||||
|
target_doc.serial_no = '\n'.join(serial_nos)
|
||||||
|
|
||||||
if doctype == "Purchase Receipt":
|
if doctype == "Purchase Receipt":
|
||||||
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
||||||
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
|
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
|
||||||
@ -305,10 +316,12 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
|||||||
target_doc.purchase_receipt_item = source_doc.name
|
target_doc.purchase_receipt_item = source_doc.name
|
||||||
|
|
||||||
elif doctype == "Purchase Invoice":
|
elif doctype == "Purchase Invoice":
|
||||||
target_doc.received_qty = -1 * source_doc.received_qty
|
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
||||||
target_doc.rejected_qty = -1 * source_doc.rejected_qty
|
target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
|
||||||
target_doc.qty = -1* source_doc.qty
|
target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
|
||||||
target_doc.stock_qty = -1 * source_doc.stock_qty
|
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
|
||||||
|
|
||||||
|
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
|
||||||
target_doc.purchase_order = source_doc.purchase_order
|
target_doc.purchase_order = source_doc.purchase_order
|
||||||
target_doc.purchase_receipt = source_doc.purchase_receipt
|
target_doc.purchase_receipt = source_doc.purchase_receipt
|
||||||
target_doc.rejected_warehouse = source_doc.rejected_warehouse
|
target_doc.rejected_warehouse = source_doc.rejected_warehouse
|
||||||
@ -330,6 +343,10 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
|||||||
if default_warehouse_for_sales_return:
|
if default_warehouse_for_sales_return:
|
||||||
target_doc.warehouse = default_warehouse_for_sales_return
|
target_doc.warehouse = default_warehouse_for_sales_return
|
||||||
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
|
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
|
||||||
|
returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
|
||||||
|
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
|
||||||
|
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
|
||||||
|
|
||||||
target_doc.sales_order = source_doc.sales_order
|
target_doc.sales_order = source_doc.sales_order
|
||||||
target_doc.delivery_note = source_doc.delivery_note
|
target_doc.delivery_note = source_doc.delivery_note
|
||||||
target_doc.so_detail = source_doc.so_detail
|
target_doc.so_detail = source_doc.so_detail
|
||||||
@ -365,3 +382,63 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
|||||||
}, target_doc, set_missing_values)
|
}, target_doc, set_missing_values)
|
||||||
|
|
||||||
return doclist
|
return doclist
|
||||||
|
|
||||||
|
def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, item_row=None, voucher_detail_no=None):
|
||||||
|
if not return_against:
|
||||||
|
return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against")
|
||||||
|
|
||||||
|
return_against_item_field = get_return_against_item_fields(voucher_type)
|
||||||
|
|
||||||
|
filters = get_filters(voucher_type, voucher_no, voucher_detail_no,
|
||||||
|
return_against, item_code, return_against_item_field, item_row)
|
||||||
|
|
||||||
|
if voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
||||||
|
select_field = "incoming_rate"
|
||||||
|
else:
|
||||||
|
select_field = "abs(stock_value_difference / actual_qty)"
|
||||||
|
|
||||||
|
return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
|
||||||
|
|
||||||
|
def get_return_against_item_fields(voucher_type):
|
||||||
|
return_against_item_fields = {
|
||||||
|
"Purchase Receipt": "purchase_receipt_item",
|
||||||
|
"Purchase Invoice": "purchase_invoice_item",
|
||||||
|
"Delivery Note": "dn_detail",
|
||||||
|
"Sales Invoice": "sales_invoice_item"
|
||||||
|
}
|
||||||
|
return return_against_item_fields[voucher_type]
|
||||||
|
|
||||||
|
def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, item_code, return_against_item_field, item_row):
|
||||||
|
filters = {
|
||||||
|
"voucher_type": voucher_type,
|
||||||
|
"voucher_no": return_against,
|
||||||
|
"item_code": item_code
|
||||||
|
}
|
||||||
|
|
||||||
|
if item_row:
|
||||||
|
reference_voucher_detail_no = item_row.get(return_against_item_field)
|
||||||
|
else:
|
||||||
|
reference_voucher_detail_no = frappe.db.get_value(voucher_type + " Item", voucher_detail_no, return_against_item_field)
|
||||||
|
|
||||||
|
if reference_voucher_detail_no:
|
||||||
|
filters["voucher_detail_no"] = reference_voucher_detail_no
|
||||||
|
|
||||||
|
return filters
|
||||||
|
|
||||||
|
def get_returned_serial_nos(child_doc, parent_doc):
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
return_ref_field = frappe.scrub(child_doc.doctype)
|
||||||
|
if child_doc.doctype == "Delivery Note Item":
|
||||||
|
return_ref_field = "dn_detail"
|
||||||
|
|
||||||
|
serial_nos = []
|
||||||
|
|
||||||
|
fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)]
|
||||||
|
|
||||||
|
filters = [[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "is_return", "=", 1],
|
||||||
|
[child_doc.doctype, return_ref_field, "=", child_doc.name], [parent_doc.doctype, "docstatus", "=", 1]]
|
||||||
|
|
||||||
|
for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters):
|
||||||
|
serial_nos.extend(get_serial_nos(row.serial_no))
|
||||||
|
|
||||||
|
return serial_nos
|
@ -13,6 +13,7 @@ from frappe.contacts.doctype.address.address import get_address_display
|
|||||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
|
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
|
||||||
|
|
||||||
class SellingController(StockController):
|
class SellingController(StockController):
|
||||||
def __setup__(self):
|
def __setup__(self):
|
||||||
@ -48,6 +49,7 @@ class SellingController(StockController):
|
|||||||
self.set_customer_address()
|
self.set_customer_address()
|
||||||
self.validate_for_duplicate_items()
|
self.validate_for_duplicate_items()
|
||||||
self.validate_target_warehouse()
|
self.validate_target_warehouse()
|
||||||
|
self.set_incoming_rate()
|
||||||
|
|
||||||
def set_missing_values(self, for_validate=False):
|
def set_missing_values(self, for_validate=False):
|
||||||
|
|
||||||
@ -230,7 +232,8 @@ class SellingController(StockController):
|
|||||||
'voucher_type': self.doctype,
|
'voucher_type': self.doctype,
|
||||||
'allow_zero_valuation': d.allow_zero_valuation_rate,
|
'allow_zero_valuation': d.allow_zero_valuation_rate,
|
||||||
'sales_invoice_item': d.get("sales_invoice_item"),
|
'sales_invoice_item': d.get("sales_invoice_item"),
|
||||||
'delivery_note_item': d.get("dn_detail")
|
'dn_detail': d.get("dn_detail"),
|
||||||
|
'incoming_rate': p.incoming_rate
|
||||||
}))
|
}))
|
||||||
else:
|
else:
|
||||||
il.append(frappe._dict({
|
il.append(frappe._dict({
|
||||||
@ -248,7 +251,8 @@ class SellingController(StockController):
|
|||||||
'voucher_type': self.doctype,
|
'voucher_type': self.doctype,
|
||||||
'allow_zero_valuation': d.allow_zero_valuation_rate,
|
'allow_zero_valuation': d.allow_zero_valuation_rate,
|
||||||
'sales_invoice_item': d.get("sales_invoice_item"),
|
'sales_invoice_item': d.get("sales_invoice_item"),
|
||||||
'delivery_note_item': d.get("dn_detail")
|
'dn_detail': d.get("dn_detail"),
|
||||||
|
'incoming_rate': d.incoming_rate
|
||||||
}))
|
}))
|
||||||
return il
|
return il
|
||||||
|
|
||||||
@ -307,69 +311,89 @@ class SellingController(StockController):
|
|||||||
|
|
||||||
sales_order.update_reserved_qty(so_item_rows)
|
sales_order.update_reserved_qty(so_item_rows)
|
||||||
|
|
||||||
|
def set_incoming_rate(self):
|
||||||
|
if self.doctype not in ("Delivery Note", "Sales Invoice"):
|
||||||
|
return
|
||||||
|
|
||||||
|
items = self.get("items") + (self.get("packed_items") or [])
|
||||||
|
for d in items:
|
||||||
|
if not cint(self.get("is_return")):
|
||||||
|
# Get incoming rate based on original item cost based on valuation method
|
||||||
|
d.incoming_rate = get_incoming_rate({
|
||||||
|
"item_code": d.item_code,
|
||||||
|
"warehouse": d.warehouse,
|
||||||
|
"posting_date": self.posting_date,
|
||||||
|
"posting_time": self.posting_time,
|
||||||
|
"qty": -1*flt(d.qty),
|
||||||
|
"serial_no": d.serial_no,
|
||||||
|
"company": self.company,
|
||||||
|
"voucher_type": self.doctype,
|
||||||
|
"voucher_no": self.name,
|
||||||
|
"allow_zero_valuation": d.get("allow_zero_valuation")
|
||||||
|
}, raise_error_if_no_rate=False)
|
||||||
|
elif self.get("return_against"):
|
||||||
|
# Get incoming rate of return entry from reference document
|
||||||
|
# based on original item cost as per valuation method
|
||||||
|
d.incoming_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
|
||||||
|
|
||||||
def update_stock_ledger(self):
|
def update_stock_ledger(self):
|
||||||
self.update_reserved_qty()
|
self.update_reserved_qty()
|
||||||
|
|
||||||
sl_entries = []
|
sl_entries = []
|
||||||
|
# Loop over items and packed items table
|
||||||
for d in self.get_item_list():
|
for d in self.get_item_list():
|
||||||
if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty):
|
if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty):
|
||||||
if flt(d.conversion_factor)==0.0:
|
if flt(d.conversion_factor)==0.0:
|
||||||
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
|
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
|
||||||
return_rate = 0
|
|
||||||
if cint(self.is_return) and self.return_against and self.docstatus==1:
|
|
||||||
against_document_no = (d.get("sales_invoice_item")
|
|
||||||
if self.doctype == "Sales Invoice" else d.get("delivery_note_item"))
|
|
||||||
|
|
||||||
return_rate = self.get_incoming_rate_for_return(d.item_code,
|
# On cancellation or return entry submission, make stock ledger entry for
|
||||||
self.return_against, against_document_no)
|
|
||||||
|
|
||||||
# On cancellation or if return entry submission, make stock ledger entry for
|
|
||||||
# target warehouse first, to update serial no values properly
|
# target warehouse first, to update serial no values properly
|
||||||
|
|
||||||
if d.warehouse and ((not cint(self.is_return) and self.docstatus==1)
|
if d.warehouse and ((not cint(self.is_return) and self.docstatus==1)
|
||||||
or (cint(self.is_return) and self.docstatus==2)):
|
or (cint(self.is_return) and self.docstatus==2)):
|
||||||
sl_entries.append(self.get_sl_entries(d, {
|
sl_entries.append(self.get_sle_for_source_warehouse(d))
|
||||||
"actual_qty": -1*flt(d.qty),
|
|
||||||
"incoming_rate": return_rate
|
|
||||||
}))
|
|
||||||
|
|
||||||
if d.target_warehouse:
|
if d.target_warehouse:
|
||||||
target_warehouse_sle = self.get_sl_entries(d, {
|
sl_entries.append(self.get_sle_for_target_warehouse(d))
|
||||||
"actual_qty": flt(d.qty),
|
|
||||||
"warehouse": d.target_warehouse
|
|
||||||
})
|
|
||||||
|
|
||||||
if self.docstatus == 1:
|
|
||||||
if not cint(self.is_return):
|
|
||||||
args = frappe._dict({
|
|
||||||
"item_code": d.item_code,
|
|
||||||
"warehouse": d.warehouse,
|
|
||||||
"posting_date": self.posting_date,
|
|
||||||
"posting_time": self.posting_time,
|
|
||||||
"qty": -1*flt(d.qty),
|
|
||||||
"serial_no": d.serial_no,
|
|
||||||
"company": d.company,
|
|
||||||
"voucher_type": d.voucher_type,
|
|
||||||
"voucher_no": d.name,
|
|
||||||
"allow_zero_valuation": d.allow_zero_valuation
|
|
||||||
})
|
|
||||||
target_warehouse_sle.update({
|
|
||||||
"incoming_rate": get_incoming_rate(args)
|
|
||||||
})
|
|
||||||
else:
|
|
||||||
target_warehouse_sle.update({
|
|
||||||
"outgoing_rate": return_rate
|
|
||||||
})
|
|
||||||
sl_entries.append(target_warehouse_sle)
|
|
||||||
|
|
||||||
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
|
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
|
||||||
or (cint(self.is_return) and self.docstatus==1)):
|
or (cint(self.is_return) and self.docstatus==1)):
|
||||||
sl_entries.append(self.get_sl_entries(d, {
|
sl_entries.append(self.get_sle_for_source_warehouse(d))
|
||||||
"actual_qty": -1*flt(d.qty),
|
|
||||||
"incoming_rate": return_rate
|
|
||||||
}))
|
|
||||||
self.make_sl_entries(sl_entries)
|
self.make_sl_entries(sl_entries)
|
||||||
|
|
||||||
|
def get_sle_for_source_warehouse(self, item_row):
|
||||||
|
sle = self.get_sl_entries(item_row, {
|
||||||
|
"actual_qty": -1*flt(item_row.qty),
|
||||||
|
"incoming_rate": item_row.incoming_rate,
|
||||||
|
"recalculate_rate": cint(self.is_return)
|
||||||
|
})
|
||||||
|
if item_row.target_warehouse and not cint(self.is_return):
|
||||||
|
sle.dependant_sle_voucher_detail_no = item_row.name
|
||||||
|
|
||||||
|
return sle
|
||||||
|
|
||||||
|
def get_sle_for_target_warehouse(self, item_row):
|
||||||
|
sle = self.get_sl_entries(item_row, {
|
||||||
|
"actual_qty": flt(item_row.qty),
|
||||||
|
"warehouse": item_row.target_warehouse
|
||||||
|
})
|
||||||
|
|
||||||
|
if self.docstatus == 1:
|
||||||
|
if not cint(self.is_return):
|
||||||
|
sle.update({
|
||||||
|
"incoming_rate": item_row.incoming_rate,
|
||||||
|
"recalculate_rate": 1
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
sle.update({
|
||||||
|
"outgoing_rate": item_row.incoming_rate
|
||||||
|
})
|
||||||
|
if item_row.warehouse:
|
||||||
|
sle.dependant_sle_voucher_detail_no = item_row.name
|
||||||
|
|
||||||
|
return sle
|
||||||
|
|
||||||
def set_po_nos(self, for_validate=False):
|
def set_po_nos(self, for_validate=False):
|
||||||
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
|
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
|
||||||
if for_validate and self.po_no:
|
if for_validate and self.po_no:
|
||||||
|
@ -7,7 +7,7 @@ from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
|
||||||
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
from erpnext.stock.stock_ledger import get_valuation_rate
|
from erpnext.stock.stock_ledger import get_valuation_rate
|
||||||
@ -26,7 +26,7 @@ class StockController(AccountsController):
|
|||||||
self.validate_customer_provided_item()
|
self.validate_customer_provided_item()
|
||||||
self.validate_putaway_capacity()
|
self.validate_putaway_capacity()
|
||||||
|
|
||||||
def make_gl_entries(self, gl_entries=None):
|
def make_gl_entries(self, gl_entries=None, from_repost=False):
|
||||||
if self.docstatus == 2:
|
if self.docstatus == 2:
|
||||||
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
|
||||||
|
|
||||||
@ -36,12 +36,12 @@ class StockController(AccountsController):
|
|||||||
if self.docstatus==1:
|
if self.docstatus==1:
|
||||||
if not gl_entries:
|
if not gl_entries:
|
||||||
gl_entries = self.get_gl_entries(warehouse_account)
|
gl_entries = self.get_gl_entries(warehouse_account)
|
||||||
make_gl_entries(gl_entries)
|
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||||
|
|
||||||
elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1:
|
elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1:
|
||||||
gl_entries = []
|
gl_entries = []
|
||||||
gl_entries = self.get_asset_gl_entry(gl_entries)
|
gl_entries = self.get_asset_gl_entry(gl_entries)
|
||||||
make_gl_entries(gl_entries)
|
make_gl_entries(gl_entries, from_repost=from_repost)
|
||||||
|
|
||||||
def validate_serialized_batch(self):
|
def validate_serialized_batch(self):
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
@ -72,7 +72,6 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
gl_list = []
|
gl_list = []
|
||||||
warehouse_with_no_account = []
|
warehouse_with_no_account = []
|
||||||
|
|
||||||
precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
|
precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
|
||||||
for item_row in voucher_details:
|
for item_row in voucher_details:
|
||||||
sle_list = sle_map.get(item_row.name)
|
sle_list = sle_map.get(item_row.name)
|
||||||
@ -127,7 +126,7 @@ class StockController(AccountsController):
|
|||||||
if warehouse_with_no_account:
|
if warehouse_with_no_account:
|
||||||
for wh in warehouse_with_no_account:
|
for wh in warehouse_with_no_account:
|
||||||
if frappe.db.get_value("Warehouse", wh, "company"):
|
if frappe.db.get_value("Warehouse", wh, "company"):
|
||||||
frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
|
frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
|
||||||
|
|
||||||
return process_gl_map(gl_list)
|
return process_gl_map(gl_list)
|
||||||
|
|
||||||
@ -311,23 +310,6 @@ class StockController(AccountsController):
|
|||||||
|
|
||||||
return serialized_items
|
return serialized_items
|
||||||
|
|
||||||
def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None):
|
|
||||||
incoming_rate = 0.0
|
|
||||||
cond = ''
|
|
||||||
if against_document and item_code:
|
|
||||||
if against_document_no:
|
|
||||||
cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no))
|
|
||||||
|
|
||||||
incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty)
|
|
||||||
from `tabStock Ledger Entry`
|
|
||||||
where voucher_type = %s and voucher_no = %s
|
|
||||||
and item_code = %s {0} limit 1""".format(cond),
|
|
||||||
(self.doctype, against_document, item_code))
|
|
||||||
|
|
||||||
incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
|
|
||||||
|
|
||||||
return incoming_rate
|
|
||||||
|
|
||||||
def validate_warehouse(self):
|
def validate_warehouse(self):
|
||||||
from erpnext.stock.utils import validate_warehouse_company
|
from erpnext.stock.utils import validate_warehouse_company
|
||||||
|
|
||||||
@ -447,19 +429,72 @@ class StockController(AccountsController):
|
|||||||
frappe.throw(msg=message, title=_("Over Receipt"))
|
frappe.throw(msg=message, title=_("Over Receipt"))
|
||||||
return rule_map
|
return rule_map
|
||||||
|
|
||||||
def compare_existing_and_expected_gle(existing_gle, expected_gle):
|
def repost_future_sle_and_gle(self):
|
||||||
matched = True
|
args = frappe._dict({
|
||||||
for entry in expected_gle:
|
"posting_date": self.posting_date,
|
||||||
account_existed = False
|
"posting_time": self.posting_time,
|
||||||
for e in existing_gle:
|
"voucher_type": self.doctype,
|
||||||
if entry.account == e.account:
|
"voucher_no": self.name,
|
||||||
account_existed = True
|
"company": self.company
|
||||||
if entry.account == e.account and entry.against_account == e.against_account \
|
})
|
||||||
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
|
|
||||||
and (entry.debit != e.debit or entry.credit != e.credit):
|
if check_if_future_sle_exists(args):
|
||||||
matched = False
|
create_repost_item_valuation_entry(args)
|
||||||
break
|
elif not is_reposting_pending():
|
||||||
if not account_existed:
|
check_if_stock_and_account_balance_synced(self.posting_date,
|
||||||
matched = False
|
self.company, self.doctype, self.name)
|
||||||
|
|
||||||
|
def is_reposting_pending():
|
||||||
|
return frappe.db.exists("Repost Item Valuation",
|
||||||
|
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
|
||||||
|
|
||||||
|
|
||||||
|
def check_if_future_sle_exists(args):
|
||||||
|
sl_entries = frappe.db.get_all("Stock Ledger Entry",
|
||||||
|
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
|
||||||
|
fields=["item_code", "warehouse"],
|
||||||
|
order_by="creation asc")
|
||||||
|
|
||||||
|
distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries]))
|
||||||
|
|
||||||
|
sle_exists = False
|
||||||
|
for item_code, warehouse in distinct_item_warehouses:
|
||||||
|
args.update({
|
||||||
|
"item_code": item_code,
|
||||||
|
"warehouse": warehouse
|
||||||
|
})
|
||||||
|
if get_sle(args):
|
||||||
|
sle_exists = True
|
||||||
break
|
break
|
||||||
return matched
|
return sle_exists
|
||||||
|
|
||||||
|
def get_sle(args):
|
||||||
|
return frappe.db.sql("""
|
||||||
|
select name
|
||||||
|
from `tabStock Ledger Entry`
|
||||||
|
where
|
||||||
|
item_code=%(item_code)s
|
||||||
|
and warehouse=%(warehouse)s
|
||||||
|
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
|
||||||
|
and voucher_no != %(voucher_no)s
|
||||||
|
and is_cancelled = 0
|
||||||
|
limit 1
|
||||||
|
""", args)
|
||||||
|
|
||||||
|
def create_repost_item_valuation_entry(args):
|
||||||
|
args = frappe._dict(args)
|
||||||
|
repost_entry = frappe.new_doc("Repost Item Valuation")
|
||||||
|
repost_entry.based_on = args.based_on
|
||||||
|
if not args.based_on:
|
||||||
|
repost_entry.based_on = 'Transaction' if args.voucher_no else "Item and Warehouse"
|
||||||
|
repost_entry.voucher_type = args.voucher_type
|
||||||
|
repost_entry.voucher_no = args.voucher_no
|
||||||
|
repost_entry.item_code = args.item_code
|
||||||
|
repost_entry.warehouse = args.warehouse
|
||||||
|
repost_entry.posting_date = args.posting_date
|
||||||
|
repost_entry.posting_time = args.posting_time
|
||||||
|
repost_entry.company = args.company
|
||||||
|
repost_entry.allow_zero_rate = args.allow_zero_rate
|
||||||
|
repost_entry.flags.ignore_links = True
|
||||||
|
repost_entry.save()
|
||||||
|
repost_entry.submit()
|
@ -126,7 +126,7 @@ class Appointment(Document):
|
|||||||
add_assignemnt({
|
add_assignemnt({
|
||||||
'doctype': self.doctype,
|
'doctype': self.doctype,
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'assign_to': existing_assignee
|
'assign_to': [existing_assignee]
|
||||||
})
|
})
|
||||||
return
|
return
|
||||||
if self._assign:
|
if self._assign:
|
||||||
@ -139,7 +139,7 @@ class Appointment(Document):
|
|||||||
add_assignemnt({
|
add_assignemnt({
|
||||||
'doctype': self.doctype,
|
'doctype': self.doctype,
|
||||||
'name': self.name,
|
'name': self.name,
|
||||||
'assign_to': agent
|
'assign_to': [agent]
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ frappe.ui.form.on('Patient Appointment', {
|
|||||||
filters: {'status': 'Active'}
|
filters: {'status': 'Active'}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query('practitioner', function() {
|
frm.set_query('practitioner', function() {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@ -29,6 +30,7 @@ frappe.ui.form.on('Patient Appointment', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
frm.set_query('service_unit', function(){
|
frm.set_query('service_unit', function(){
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
@ -39,6 +41,16 @@ frappe.ui.form.on('Patient Appointment', {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query('therapy_plan', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
'patient': frm.doc.patient
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.trigger('set_therapy_type_filter');
|
||||||
|
|
||||||
if (frm.is_new()) {
|
if (frm.is_new()) {
|
||||||
frm.page.set_primary_action(__('Check Availability'), function() {
|
frm.page.set_primary_action(__('Check Availability'), function() {
|
||||||
if (!frm.doc.patient) {
|
if (!frm.doc.patient) {
|
||||||
@ -136,6 +148,24 @@ frappe.ui.form.on('Patient Appointment', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
therapy_plan: function(frm) {
|
||||||
|
frm.trigger('set_therapy_type_filter');
|
||||||
|
},
|
||||||
|
|
||||||
|
set_therapy_type_filter: function(frm) {
|
||||||
|
if (frm.doc.therapy_plan) {
|
||||||
|
frm.call('get_therapy_types').then(r => {
|
||||||
|
frm.set_query('therapy_type', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
'name': ['in', r.message]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
therapy_type: function(frm) {
|
therapy_type: function(frm) {
|
||||||
if (frm.doc.therapy_type) {
|
if (frm.doc.therapy_type) {
|
||||||
frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => {
|
frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => {
|
||||||
|
@ -23,9 +23,9 @@
|
|||||||
"procedure_template",
|
"procedure_template",
|
||||||
"get_procedure_from_encounter",
|
"get_procedure_from_encounter",
|
||||||
"procedure_prescription",
|
"procedure_prescription",
|
||||||
|
"therapy_plan",
|
||||||
"therapy_type",
|
"therapy_type",
|
||||||
"get_prescribed_therapies",
|
"get_prescribed_therapies",
|
||||||
"therapy_plan",
|
|
||||||
"practitioner",
|
"practitioner",
|
||||||
"practitioner_name",
|
"practitioner_name",
|
||||||
"department",
|
"department",
|
||||||
@ -284,7 +284,7 @@
|
|||||||
"report_hide": 1
|
"report_hide": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.patient;",
|
"depends_on": "eval:doc.patient && doc.therapy_plan;",
|
||||||
"fieldname": "therapy_type",
|
"fieldname": "therapy_type",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Therapy",
|
"label": "Therapy",
|
||||||
@ -292,17 +292,16 @@
|
|||||||
"set_only_once": 1
|
"set_only_once": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.patient && doc.__islocal;",
|
"depends_on": "eval:doc.patient && doc.therapy_plan && doc.__islocal;",
|
||||||
"fieldname": "get_prescribed_therapies",
|
"fieldname": "get_prescribed_therapies",
|
||||||
"fieldtype": "Button",
|
"fieldtype": "Button",
|
||||||
"label": "Get Prescribed Therapies"
|
"label": "Get Prescribed Therapies"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval: doc.patient && doc.therapy_type",
|
"depends_on": "eval: doc.patient;",
|
||||||
"fieldname": "therapy_plan",
|
"fieldname": "therapy_plan",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Therapy Plan",
|
"label": "Therapy Plan",
|
||||||
"mandatory_depends_on": "eval: doc.patient && doc.therapy_type",
|
|
||||||
"options": "Therapy Plan"
|
"options": "Therapy Plan"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -348,7 +347,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-21 03:04:21.400893",
|
"modified": "2020-12-16 13:16:58.578503",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Healthcare",
|
"module": "Healthcare",
|
||||||
"name": "Patient Appointment",
|
"name": "Patient Appointment",
|
||||||
|
@ -91,6 +91,17 @@ class PatientAppointment(Document):
|
|||||||
if fee_validity:
|
if fee_validity:
|
||||||
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
|
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
|
||||||
|
|
||||||
|
def get_therapy_types(self):
|
||||||
|
if not self.therapy_plan:
|
||||||
|
return
|
||||||
|
|
||||||
|
therapy_types = []
|
||||||
|
doc = frappe.get_doc('Therapy Plan', self.therapy_plan)
|
||||||
|
for entry in doc.therapy_plan_details:
|
||||||
|
therapy_types.append(entry.therapy_type)
|
||||||
|
|
||||||
|
return therapy_types
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def check_payment_fields_reqd(patient):
|
def check_payment_fields_reqd(patient):
|
||||||
@ -145,7 +156,7 @@ def invoice_appointment(appointment_doc):
|
|||||||
sales_invoice.flags.ignore_mandatory = True
|
sales_invoice.flags.ignore_mandatory = True
|
||||||
sales_invoice.save(ignore_permissions=True)
|
sales_invoice.save(ignore_permissions=True)
|
||||||
sales_invoice.submit()
|
sales_invoice.submit()
|
||||||
frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True)
|
frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
|
||||||
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
|
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
|
||||||
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
|
frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
|
||||||
|
|
||||||
|
@ -397,7 +397,8 @@ regional_overrides = {
|
|||||||
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
|
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
|
||||||
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
|
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
|
||||||
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
|
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
|
||||||
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries'
|
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
|
||||||
|
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields'
|
||||||
},
|
},
|
||||||
'United Arab Emirates': {
|
'United Arab Emirates': {
|
||||||
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',
|
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',
|
||||||
|
@ -135,7 +135,7 @@ class Employee(NestedSet):
|
|||||||
try:
|
try:
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
"doctype": "File",
|
"doctype": "File",
|
||||||
"file_name": self.image,
|
"file_url": self.image,
|
||||||
"attached_to_doctype": "User",
|
"attached_to_doctype": "User",
|
||||||
"attached_to_name": self.user_id
|
"attached_to_name": self.user_id
|
||||||
}).insert()
|
}).insert()
|
||||||
|
@ -18,13 +18,18 @@ frappe.ui.form.on('Employee Advance', {
|
|||||||
if (!frm.doc.employee) {
|
if (!frm.doc.employee) {
|
||||||
frappe.msgprint(__("Please select employee first"));
|
frappe.msgprint(__("Please select employee first"));
|
||||||
}
|
}
|
||||||
var company_currency = erpnext.get_currency(frm.doc.company);
|
let company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
|
let currencies = [company_currency];
|
||||||
|
if (frm.doc.currency && (frm.doc.currency != company_currency)) {
|
||||||
|
currencies.push(frm.doc.currency);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"root_type": "Asset",
|
"root_type": "Asset",
|
||||||
"is_group": 0,
|
"is_group": 0,
|
||||||
"company": frm.doc.company,
|
"company": frm.doc.company,
|
||||||
"account_currency": ["in", [frm.doc.currency, company_currency]],
|
"account_currency": ["in", currencies],
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -181,21 +186,23 @@ frappe.ui.form.on('Employee Advance', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
currency: function(frm) {
|
currency: function(frm) {
|
||||||
var from_currency = frm.doc.currency;
|
if (frm.doc.currency) {
|
||||||
var company_currency;
|
var from_currency = frm.doc.currency;
|
||||||
if (!frm.doc.company) {
|
var company_currency;
|
||||||
company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
|
if (!frm.doc.company) {
|
||||||
} else {
|
company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
|
||||||
company_currency = erpnext.get_currency(frm.doc.company);
|
} else {
|
||||||
|
company_currency = erpnext.get_currency(frm.doc.company);
|
||||||
|
}
|
||||||
|
if (from_currency != company_currency) {
|
||||||
|
frm.events.set_exchange_rate(frm, from_currency, company_currency);
|
||||||
|
} else {
|
||||||
|
frm.set_value("exchange_rate", 1.0);
|
||||||
|
frm.set_df_property('exchange_rate', 'hidden', 1);
|
||||||
|
frm.set_df_property("exchange_rate", "description", "" );
|
||||||
|
}
|
||||||
|
frm.refresh_fields();
|
||||||
}
|
}
|
||||||
if (from_currency != company_currency) {
|
|
||||||
frm.events.set_exchange_rate(frm, from_currency, company_currency);
|
|
||||||
} else {
|
|
||||||
frm.set_value("exchange_rate", 1.0);
|
|
||||||
frm.set_df_property('exchange_rate', 'hidden', 1);
|
|
||||||
frm.set_df_property("exchange_rate", "description", "" );
|
|
||||||
}
|
|
||||||
frm.refresh_fields();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
set_exchange_rate: function(frm, from_currency, company_currency) {
|
set_exchange_rate: function(frm, from_currency, company_currency) {
|
||||||
|
@ -4,22 +4,10 @@ from frappe import _
|
|||||||
def get_data():
|
def get_data():
|
||||||
return {
|
return {
|
||||||
'fieldname': 'leave_policy',
|
'fieldname': 'leave_policy',
|
||||||
'non_standard_fieldnames': {
|
|
||||||
'Employee Grade': 'default_leave_policy'
|
|
||||||
},
|
|
||||||
'transactions': [
|
'transactions': [
|
||||||
{
|
|
||||||
'label': _('Employees'),
|
|
||||||
'items': ['Employee', 'Employee Grade']
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
'label': _('Leaves'),
|
'label': _('Leaves'),
|
||||||
'items': ['Leave Allocation']
|
'items': ['Leave Allocation']
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -111,13 +111,14 @@
|
|||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-15 15:18:15.227848",
|
"modified": "2020-12-31 16:43:30.695206",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Policy Assignment",
|
"name": "Leave Policy Assignment",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
"cancel": 1,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
@ -127,9 +128,11 @@
|
|||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "HR Manager",
|
"role": "HR Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"cancel": 1,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
@ -139,9 +142,11 @@
|
|||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "HR User",
|
"role": "HR User",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"cancel": 1,
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
@ -151,6 +156,7 @@
|
|||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
|
"submit": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
@ -6,6 +6,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe, math, json
|
import frappe, math, json
|
||||||
import erpnext
|
import erpnext
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from six import string_types
|
||||||
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
|
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
|
||||||
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
|
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
|
||||||
from erpnext.controllers.accounts_controller import AccountsController
|
from erpnext.controllers.accounts_controller import AccountsController
|
||||||
@ -280,10 +281,13 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict
|
|||||||
return write_off
|
return write_off
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0):
|
def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0):
|
||||||
# if loan is passed it will be considered as full unpledge
|
# if no security_map is passed it will be considered as full unpledge
|
||||||
|
if security_map and isinstance(security_map, string_types):
|
||||||
|
security_map = json.loads(security_map)
|
||||||
|
|
||||||
if loan:
|
if loan:
|
||||||
pledge_qty_map = get_pledged_security_qty(loan)
|
pledge_qty_map = security_map or get_pledged_security_qty(loan)
|
||||||
loan_doc = frappe.get_doc('Loan', loan)
|
loan_doc = frappe.get_doc('Loan', loan)
|
||||||
unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company,
|
unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company,
|
||||||
loan_doc.applicant_type, loan_doc.applicant)
|
loan_doc.applicant_type, loan_doc.applicant)
|
||||||
|
@ -45,7 +45,7 @@ class TestLoan(unittest.TestCase):
|
|||||||
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
|
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
|
||||||
|
|
||||||
self.applicant1 = make_employee("robert_loan@loan.com")
|
self.applicant1 = make_employee("robert_loan@loan.com")
|
||||||
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR')
|
make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company")
|
||||||
if not frappe.db.exists("Customer", "_Test Loan Customer"):
|
if not frappe.db.exists("Customer", "_Test Loan Customer"):
|
||||||
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
|
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
|
||||||
|
|
||||||
@ -325,6 +325,43 @@ class TestLoan(unittest.TestCase):
|
|||||||
self.assertEquals(amounts['payable_principal_amount'], 0.0)
|
self.assertEquals(amounts['payable_principal_amount'], 0.0)
|
||||||
self.assertEqual(amounts['interest_amount'], 0)
|
self.assertEqual(amounts['interest_amount'], 0)
|
||||||
|
|
||||||
|
def test_partial_loan_security_unpledge(self):
|
||||||
|
pledge = [{
|
||||||
|
"loan_security": "Test Security 1",
|
||||||
|
"qty": 2000.00
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"loan_security": "Test Security 2",
|
||||||
|
"qty": 4000.00
|
||||||
|
}]
|
||||||
|
|
||||||
|
loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
|
||||||
|
create_pledge(loan_application)
|
||||||
|
|
||||||
|
loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
|
||||||
|
loan.submit()
|
||||||
|
|
||||||
|
self.assertEquals(loan.loan_amount, 1000000)
|
||||||
|
|
||||||
|
first_date = '2019-10-01'
|
||||||
|
last_date = '2019-10-30'
|
||||||
|
|
||||||
|
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
|
||||||
|
process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
|
||||||
|
|
||||||
|
repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000)
|
||||||
|
repayment_entry.submit()
|
||||||
|
|
||||||
|
unpledge_map = {'Test Security 2': 2000}
|
||||||
|
|
||||||
|
unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
|
||||||
|
unpledge_request.submit()
|
||||||
|
unpledge_request.status = 'Approved'
|
||||||
|
unpledge_request.save()
|
||||||
|
unpledge_request.submit()
|
||||||
|
unpledge_request.load_from_db()
|
||||||
|
self.assertEqual(unpledge_request.docstatus, 1)
|
||||||
|
|
||||||
def test_disbursal_check_with_shortfall(self):
|
def test_disbursal_check_with_shortfall(self):
|
||||||
pledges = [{
|
pledges = [{
|
||||||
"loan_security": "Test Security 2",
|
"loan_security": "Test Security 2",
|
||||||
|
@ -81,7 +81,6 @@ def check_for_ltv_shortfall(process_loan_security_shortfall):
|
|||||||
process_loan_security_shortfall)
|
process_loan_security_shortfall)
|
||||||
|
|
||||||
def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall):
|
def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall):
|
||||||
|
|
||||||
existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name")
|
existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name")
|
||||||
|
|
||||||
if existing_shortfall:
|
if existing_shortfall:
|
||||||
|
@ -30,6 +30,8 @@ class LoanSecurityUnpledge(Document):
|
|||||||
d.idx, frappe.bold(d.loan_security)))
|
d.idx, frappe.bold(d.loan_security)))
|
||||||
|
|
||||||
def validate_unpledge_qty(self):
|
def validate_unpledge_qty(self):
|
||||||
|
from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import get_ltv_ratio
|
||||||
|
|
||||||
pledge_qty_map = get_pledged_security_qty(self.loan)
|
pledge_qty_map = get_pledged_security_qty(self.loan)
|
||||||
|
|
||||||
ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
|
ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
|
||||||
@ -47,6 +49,8 @@ class LoanSecurityUnpledge(Document):
|
|||||||
|
|
||||||
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
|
pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
|
||||||
security_value = 0
|
security_value = 0
|
||||||
|
unpledge_qty_map = {}
|
||||||
|
ltv_ratio = 0
|
||||||
|
|
||||||
for security in self.securities:
|
for security in self.securities:
|
||||||
pledged_qty = pledge_qty_map.get(security.loan_security, 0)
|
pledged_qty = pledge_qty_map.get(security.loan_security, 0)
|
||||||
@ -57,13 +61,15 @@ class LoanSecurityUnpledge(Document):
|
|||||||
msg += _("You are trying to unpledge more.")
|
msg += _("You are trying to unpledge more.")
|
||||||
frappe.throw(msg, title=_("Loan Security Unpledge Error"))
|
frappe.throw(msg, title=_("Loan Security Unpledge Error"))
|
||||||
|
|
||||||
qty_after_unpledge = pledged_qty - security.qty
|
unpledge_qty_map.setdefault(security.loan_security, 0)
|
||||||
ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
|
unpledge_qty_map[security.loan_security] += security.qty
|
||||||
|
|
||||||
current_price = loan_security_price_map.get(security.loan_security)
|
for security in pledge_qty_map:
|
||||||
if not current_price:
|
if not ltv_ratio:
|
||||||
frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security)))
|
ltv_ratio = get_ltv_ratio(security)
|
||||||
|
|
||||||
|
qty_after_unpledge = pledge_qty_map.get(security, 0) - unpledge_qty_map.get(security, 0)
|
||||||
|
current_price = loan_security_price_map.get(security)
|
||||||
security_value += qty_after_unpledge * current_price
|
security_value += qty_after_unpledge * current_price
|
||||||
|
|
||||||
if not security_value and flt(pending_principal_amount, 2) > 0:
|
if not security_value and flt(pending_principal_amount, 2) > 0:
|
||||||
|
@ -17,6 +17,7 @@ class OverlapError(frappe.ValidationError): pass
|
|||||||
|
|
||||||
class OperationMismatchError(frappe.ValidationError): pass
|
class OperationMismatchError(frappe.ValidationError): pass
|
||||||
class OperationSequenceError(frappe.ValidationError): pass
|
class OperationSequenceError(frappe.ValidationError): pass
|
||||||
|
class JobCardCancelError(frappe.ValidationError): pass
|
||||||
|
|
||||||
class JobCard(Document):
|
class JobCard(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -217,33 +218,49 @@ class JobCard(Document):
|
|||||||
field = "operation_id"
|
field = "operation_id"
|
||||||
data = self.get_current_operation_data()
|
data = self.get_current_operation_data()
|
||||||
if data and len(data) > 0:
|
if data and len(data) > 0:
|
||||||
for_quantity = data[0].completed_qty
|
for_quantity = flt(data[0].completed_qty)
|
||||||
time_in_mins = data[0].time_in_mins
|
time_in_mins = flt(data[0].time_in_mins)
|
||||||
|
|
||||||
if self.get(field):
|
wo = frappe.get_doc('Work Order', self.work_order)
|
||||||
time_data = frappe.db.sql("""
|
if self.operation_id:
|
||||||
|
self.validate_produced_quantity(for_quantity, wo)
|
||||||
|
self.update_work_order_data(for_quantity, time_in_mins, wo)
|
||||||
|
|
||||||
|
def validate_produced_quantity(self, for_quantity, wo):
|
||||||
|
if self.docstatus < 2: return
|
||||||
|
|
||||||
|
if wo.produced_qty > for_quantity:
|
||||||
|
first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.")
|
||||||
|
.format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)))
|
||||||
|
|
||||||
|
second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.")
|
||||||
|
.format(frappe.bold(get_link_to_form("Work Order", self.work_order))))
|
||||||
|
|
||||||
|
frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg),
|
||||||
|
JobCardCancelError, title = _("Error"))
|
||||||
|
|
||||||
|
def update_work_order_data(self, for_quantity, time_in_mins, wo):
|
||||||
|
time_data = frappe.db.sql("""
|
||||||
SELECT
|
SELECT
|
||||||
min(from_time) as start_time, max(to_time) as end_time
|
min(from_time) as start_time, max(to_time) as end_time
|
||||||
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
|
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
|
||||||
WHERE
|
WHERE
|
||||||
jctl.parent = jc.name and jc.work_order = %s
|
jctl.parent = jc.name and jc.work_order = %s
|
||||||
and jc.{0} = %s and jc.docstatus = 1
|
and jc.operation_id = %s and jc.docstatus = 1
|
||||||
""".format(field), (self.work_order, self.get(field)), as_dict=1)
|
""", (self.work_order, self.operation_id), as_dict=1)
|
||||||
|
|
||||||
wo = frappe.get_doc('Work Order', self.work_order)
|
for data in wo.operations:
|
||||||
|
if data.get("name") == self.operation_id:
|
||||||
|
data.completed_qty = for_quantity
|
||||||
|
data.actual_operation_time = time_in_mins
|
||||||
|
data.actual_start_time = time_data[0].start_time if time_data else None
|
||||||
|
data.actual_end_time = time_data[0].end_time if time_data else None
|
||||||
|
|
||||||
for data in wo.operations:
|
wo.flags.ignore_validate_update_after_submit = True
|
||||||
if data.get("name") == self.get(field):
|
wo.update_operation_status()
|
||||||
data.completed_qty = for_quantity
|
wo.calculate_operating_cost()
|
||||||
data.actual_operation_time = time_in_mins
|
wo.set_actual_dates()
|
||||||
data.actual_start_time = time_data[0].start_time if time_data else None
|
wo.save()
|
||||||
data.actual_end_time = time_data[0].end_time if time_data else None
|
|
||||||
|
|
||||||
wo.flags.ignore_validate_update_after_submit = True
|
|
||||||
wo.update_operation_status()
|
|
||||||
wo.calculate_operating_cost()
|
|
||||||
wo.set_actual_dates()
|
|
||||||
wo.save()
|
|
||||||
|
|
||||||
def get_current_operation_data(self):
|
def get_current_operation_data(self):
|
||||||
return frappe.get_all('Job Card',
|
return frappe.get_all('Job Card',
|
||||||
|
@ -8,7 +8,17 @@ frappe.views.calendar["Job Card"] = {
|
|||||||
"allDay": "allDay",
|
"allDay": "allDay",
|
||||||
"progress": "progress"
|
"progress": "progress"
|
||||||
},
|
},
|
||||||
gantt: true,
|
gantt: {
|
||||||
|
field_map: {
|
||||||
|
"start": "started_time",
|
||||||
|
"end": "started_time",
|
||||||
|
"id": "name",
|
||||||
|
"title": "subject",
|
||||||
|
"color": "color",
|
||||||
|
"allDay": "allDay",
|
||||||
|
"progress": "progress"
|
||||||
|
}
|
||||||
|
},
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
|
@ -5,8 +5,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import unittest
|
import unittest
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
|
from frappe.utils import flt, now, add_months, cint, today, add_to_date
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
|
||||||
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
|
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
|
||||||
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
|
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
|
||||||
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
from erpnext.stock.doctype.stock_entry import test_stock_entry
|
||||||
@ -15,10 +14,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
|||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||||
|
from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
|
||||||
|
|
||||||
class TestWorkOrder(unittest.TestCase):
|
class TestWorkOrder(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
set_perpetual_inventory(0)
|
|
||||||
self.warehouse = '_Test Warehouse 2 - _TC'
|
self.warehouse = '_Test Warehouse 2 - _TC'
|
||||||
self.item = '_Test Item'
|
self.item = '_Test Item'
|
||||||
|
|
||||||
@ -371,21 +370,49 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
self.assertEqual(ste.total_additional_costs, 1000)
|
self.assertEqual(ste.total_additional_costs, 1000)
|
||||||
|
|
||||||
def test_job_card(self):
|
def test_job_card(self):
|
||||||
|
stock_entries = []
|
||||||
data = frappe.get_cached_value('BOM',
|
data = frappe.get_cached_value('BOM',
|
||||||
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
|
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
|
||||||
|
|
||||||
if data:
|
bom, bom_item = data
|
||||||
frappe.db.set_value("Manufacturing Settings",
|
|
||||||
None, "disable_capacity_planning", 0)
|
|
||||||
|
|
||||||
bom, bom_item = data
|
bom_doc = frappe.get_doc('BOM', bom)
|
||||||
|
work_order = make_wo_order_test_record(item=bom_item, qty=1,
|
||||||
|
bom_no=bom, source_warehouse="_Test Warehouse - _TC")
|
||||||
|
|
||||||
bom_doc = frappe.get_doc('BOM', bom)
|
for row in work_order.required_items:
|
||||||
work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
|
stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code,
|
||||||
self.assertTrue(work_order.planned_end_date)
|
target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100)
|
||||||
|
stock_entries.append(stock_entry_doc)
|
||||||
|
|
||||||
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
|
ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
|
||||||
self.assertEqual(len(job_cards), len(bom_doc.operations))
|
ste.submit()
|
||||||
|
stock_entries.append(ste)
|
||||||
|
|
||||||
|
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
|
||||||
|
self.assertEqual(len(job_cards), len(bom_doc.operations))
|
||||||
|
|
||||||
|
for i, job_card in enumerate(job_cards):
|
||||||
|
doc = frappe.get_doc("Job Card", job_card)
|
||||||
|
doc.append("time_logs", {
|
||||||
|
"from_time": now(),
|
||||||
|
"hours": i,
|
||||||
|
"to_time": add_to_date(now(), i),
|
||||||
|
"completed_qty": doc.for_quantity
|
||||||
|
})
|
||||||
|
doc.submit()
|
||||||
|
|
||||||
|
ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
|
||||||
|
ste1.submit()
|
||||||
|
stock_entries.append(ste1)
|
||||||
|
|
||||||
|
for job_card in job_cards:
|
||||||
|
doc = frappe.get_doc("Job Card", job_card)
|
||||||
|
self.assertRaises(JobCardCancelError, doc.cancel)
|
||||||
|
|
||||||
|
stock_entries.reverse()
|
||||||
|
for stock_entry in stock_entries:
|
||||||
|
stock_entry.cancel()
|
||||||
|
|
||||||
def test_capcity_planning(self):
|
def test_capcity_planning(self):
|
||||||
frappe.db.set_value("Manufacturing Settings", None, {
|
frappe.db.set_value("Manufacturing Settings", None, {
|
||||||
@ -511,7 +538,6 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
ste1.submit()
|
ste1.submit()
|
||||||
ste_cancel_list.append(ste1)
|
ste_cancel_list.append(ste1)
|
||||||
|
|
||||||
print(wo_order.name)
|
|
||||||
ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
|
ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
|
||||||
self.assertEquals(ste3.fg_completed_qty, 2)
|
self.assertEquals(ste3.fg_completed_qty, 2)
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class Member(Document):
|
|||||||
frappe.msgprint(_("A customer is already linked to this Member"))
|
frappe.msgprint(_("A customer is already linked to this Member"))
|
||||||
cust = create_customer(frappe._dict({
|
cust = create_customer(frappe._dict({
|
||||||
'fullname': self.member_name,
|
'fullname': self.member_name,
|
||||||
'email': self.email_id or self.user,
|
'email': self.email_id or self.email,
|
||||||
'phone': None
|
'phone': None
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
@ -450,7 +450,6 @@ erpnext.patches.v8_9.set_member_party_type
|
|||||||
erpnext.patches.v9_0.add_user_to_child_table_in_pos_profile
|
erpnext.patches.v9_0.add_user_to_child_table_in_pos_profile
|
||||||
erpnext.patches.v9_0.set_schedule_date_for_material_request_and_purchase_order
|
erpnext.patches.v9_0.set_schedule_date_for_material_request_and_purchase_order
|
||||||
erpnext.patches.v9_0.student_admission_childtable_migrate
|
erpnext.patches.v9_0.student_admission_childtable_migrate
|
||||||
erpnext.patches.v9_0.fix_subscription_next_date #2017-10-23
|
|
||||||
erpnext.patches.v9_0.add_healthcare_domain
|
erpnext.patches.v9_0.add_healthcare_domain
|
||||||
erpnext.patches.v9_0.set_variant_item_description
|
erpnext.patches.v9_0.set_variant_item_description
|
||||||
erpnext.patches.v9_0.set_uoms_in_variant_field
|
erpnext.patches.v9_0.set_uoms_in_variant_field
|
||||||
@ -712,6 +711,7 @@ erpnext.patches.v13_0.delete_old_sales_reports
|
|||||||
execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation")
|
execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation")
|
||||||
erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020
|
erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020
|
||||||
erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020
|
erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020
|
||||||
|
execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings")
|
||||||
erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
|
erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
|
||||||
erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
|
erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
|
||||||
erpnext.patches.v12_0.add_taxjar_integration_field
|
erpnext.patches.v12_0.add_taxjar_integration_field
|
||||||
@ -733,6 +733,7 @@ erpnext.patches.v13_0.set_youtube_video_id
|
|||||||
erpnext.patches.v13_0.print_uom_after_quantity_patch
|
erpnext.patches.v13_0.print_uom_after_quantity_patch
|
||||||
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
|
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
|
||||||
erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
|
erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
|
||||||
|
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
|
||||||
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
||||||
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
|
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
|
||||||
erpnext.patches.v13_0.update_custom_fields_for_shopify
|
erpnext.patches.v13_0.update_custom_fields_for_shopify
|
||||||
|
56
erpnext/patches/v12_0/setup_einvoice_fields.py
Normal file
56
erpnext/patches/v12_0/setup_einvoice_fields.py
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
from erpnext.regional.india.setup import add_permissions, add_print_formats
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||||
|
if not company:
|
||||||
|
return
|
||||||
|
|
||||||
|
frappe.reload_doc("custom", "doctype", "custom_field")
|
||||||
|
frappe.reload_doc("regional", "doctype", "e_invoice_settings")
|
||||||
|
custom_fields = {
|
||||||
|
'Sales Invoice': [
|
||||||
|
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
|
||||||
|
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
|
||||||
|
|
||||||
|
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
|
||||||
|
|
||||||
|
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
||||||
|
|
||||||
|
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||||
|
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
|
||||||
|
|
||||||
|
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||||
|
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
|
||||||
|
|
||||||
|
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
|
||||||
|
|
||||||
|
dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
|
||||||
|
|
||||||
|
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
|
||||||
|
]
|
||||||
|
}
|
||||||
|
create_custom_fields(custom_fields, update=True)
|
||||||
|
add_permissions()
|
||||||
|
add_print_formats()
|
||||||
|
|
||||||
|
einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
|
||||||
|
t = {
|
||||||
|
'mode_of_transport': [{'default': None}],
|
||||||
|
'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}],
|
||||||
|
'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
|
||||||
|
'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
|
||||||
|
'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
|
||||||
|
'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
|
||||||
|
'ewaybill': [
|
||||||
|
{'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'},
|
||||||
|
{'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
for field, conditions in t.items():
|
||||||
|
for c in conditions:
|
||||||
|
[(prop, value)] = c.items()
|
||||||
|
frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value)
|
@ -1,7 +1,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import nowdate
|
from frappe.utils import nowdate, flt
|
||||||
from erpnext.accounts.doctype.account.test_account import create_account
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
|
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
|
||||||
from erpnext.loan_management.doctype.loan.loan import make_repayment_entry
|
from erpnext.loan_management.doctype.loan.loan import make_repayment_entry
|
||||||
@ -113,15 +113,15 @@ def execute():
|
|||||||
interest_paid = 0
|
interest_paid = 0
|
||||||
principal_paid = 0
|
principal_paid = 0
|
||||||
|
|
||||||
if total_interest > entry.interest_amount:
|
if flt(total_interest) > flt(entry.interest_amount):
|
||||||
interest_paid = entry.interest_amount
|
interest_paid = flt(entry.interest_amount)
|
||||||
else:
|
else:
|
||||||
interest_paid = total_interest
|
interest_paid = flt(total_interest)
|
||||||
|
|
||||||
if total_principal > entry.payable_principal_amount:
|
if flt(total_principal) > flt(entry.payable_principal_amount):
|
||||||
principal_paid = entry.payable_principal_amount
|
principal_paid = flt(entry.payable_principal_amount)
|
||||||
else:
|
else:
|
||||||
principal_paid = total_principal
|
principal_paid = flt(total_principal)
|
||||||
|
|
||||||
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
|
frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
|
||||||
SET paid_principal_amount = `paid_principal_amount` + %s,
|
SET paid_principal_amount = `paid_principal_amount` + %s,
|
||||||
@ -129,8 +129,8 @@ def execute():
|
|||||||
WHERE name = %s""",
|
WHERE name = %s""",
|
||||||
(principal_paid, interest_paid, entry.name))
|
(principal_paid, interest_paid, entry.name))
|
||||||
|
|
||||||
total_principal -= principal_paid
|
total_principal = flt(total_principal) - principal_paid
|
||||||
total_interest -= interest_paid
|
total_interest = flt(total_interest) - interest_paid
|
||||||
|
|
||||||
def create_loan_type(loan, loan_type_name, penalty_account):
|
def create_loan_type(loan, loan_type_name, penalty_account):
|
||||||
loan_type_doc = frappe.new_doc('Loan Type')
|
loan_type_doc = frappe.new_doc('Loan Type')
|
||||||
|
@ -1,48 +0,0 @@
|
|||||||
# Copyright (c) 2017, Frappe and Contributors
|
|
||||||
# License: GNU General Public License v3. See license.txt
|
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import frappe
|
|
||||||
from frappe.utils import getdate
|
|
||||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date
|
|
||||||
|
|
||||||
def execute():
|
|
||||||
frappe.reload_doc('accounts', 'doctype', 'subscription')
|
|
||||||
fields = ["name", "reference_doctype", "reference_document",
|
|
||||||
"start_date", "frequency", "repeat_on_day"]
|
|
||||||
|
|
||||||
for d in fields:
|
|
||||||
if not frappe.db.has_column('Subscription', d):
|
|
||||||
return
|
|
||||||
|
|
||||||
doctypes = ('Purchase Order', 'Sales Order', 'Purchase Invoice', 'Sales Invoice')
|
|
||||||
for data in frappe.get_all('Subscription',
|
|
||||||
fields = fields,
|
|
||||||
filters = {'reference_doctype': ('in', doctypes), 'docstatus': 1}):
|
|
||||||
|
|
||||||
recurring_id = frappe.db.get_value(data.reference_doctype, data.reference_document, "recurring_id")
|
|
||||||
if recurring_id:
|
|
||||||
frappe.db.sql("update `tab{0}` set subscription=%s where recurring_id=%s"
|
|
||||||
.format(data.reference_doctype), (data.name, recurring_id))
|
|
||||||
|
|
||||||
date_field = 'transaction_date'
|
|
||||||
if data.reference_doctype in ['Sales Invoice', 'Purchase Invoice']:
|
|
||||||
date_field = 'posting_date'
|
|
||||||
|
|
||||||
start_date = frappe.db.get_value(data.reference_doctype, data.reference_document, date_field)
|
|
||||||
|
|
||||||
if start_date and getdate(start_date) != getdate(data.start_date):
|
|
||||||
last_ref_date = frappe.db.sql("""
|
|
||||||
select {0}
|
|
||||||
from `tab{1}`
|
|
||||||
where subscription=%s and docstatus < 2
|
|
||||||
order by creation desc
|
|
||||||
limit 1
|
|
||||||
""".format(date_field, data.reference_doctype), data.name)[0][0]
|
|
||||||
|
|
||||||
next_schedule_date = get_next_schedule_date(last_ref_date, data.frequency, data.repeat_on_day)
|
|
||||||
|
|
||||||
frappe.db.set_value("Subscription", data.name, {
|
|
||||||
"start_date": start_date,
|
|
||||||
"next_schedule_date": next_schedule_date
|
|
||||||
}, None)
|
|
@ -12,14 +12,6 @@ frappe.ui.form.on('Additional Salary', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!frm.doc.currency) return;
|
|
||||||
frm.set_query("salary_component", function() {
|
|
||||||
return {
|
|
||||||
query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
|
|
||||||
filters: {currency: frm.doc.currency, company: frm.doc.company}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
employee: function(frm) {
|
employee: function(frm) {
|
||||||
|
@ -23,6 +23,7 @@
|
|||||||
"employee_benefits",
|
"employee_benefits",
|
||||||
"totals",
|
"totals",
|
||||||
"total_amount",
|
"total_amount",
|
||||||
|
"column_break",
|
||||||
"pro_rata_dispensed_amount"
|
"pro_rata_dispensed_amount"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -139,11 +140,15 @@
|
|||||||
"label": "Company",
|
"label": "Company",
|
||||||
"options": "Company",
|
"options": "Company",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-25 11:49:05.095101",
|
"modified": "2020-12-14 15:52:08.566418",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Employee Benefit Application",
|
"name": "Employee Benefit Application",
|
||||||
|
@ -11,11 +11,11 @@ frappe.ui.form.on('Employee Incentive', {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!frm.doc.currency) return;
|
if (!frm.doc.company) return;
|
||||||
frm.set_query("salary_component", function() {
|
frm.set_query("salary_component", function() {
|
||||||
return {
|
return {
|
||||||
query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
|
query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
|
||||||
filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company}
|
filters: {type: "earning", company: frm.doc.company}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -86,19 +86,21 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(declaration.total_exemption_amount, 100000)
|
self.assertEqual(declaration.total_exemption_amount, 100000)
|
||||||
|
|
||||||
def create_payroll_period():
|
def create_payroll_period(**args):
|
||||||
if not frappe.db.exists("Payroll Period", "_Test Payroll Period"):
|
args = frappe._dict(args)
|
||||||
|
name = args.name or "_Test Payroll Period"
|
||||||
|
if not frappe.db.exists("Payroll Period", name):
|
||||||
from datetime import date
|
from datetime import date
|
||||||
payroll_period = frappe.get_doc(dict(
|
payroll_period = frappe.get_doc(dict(
|
||||||
doctype = 'Payroll Period',
|
doctype = 'Payroll Period',
|
||||||
name = "_Test Payroll Period",
|
name = name,
|
||||||
company = erpnext.get_default_company(),
|
company = args.company or erpnext.get_default_company(),
|
||||||
start_date = date(date.today().year, 1, 1),
|
start_date = args.start_date or date(date.today().year, 1, 1),
|
||||||
end_date = date(date.today().year, 12, 31)
|
end_date = args.end_date or date(date.today().year, 12, 31)
|
||||||
)).insert()
|
)).insert()
|
||||||
return payroll_period
|
return payroll_period
|
||||||
else:
|
else:
|
||||||
return frappe.get_doc("Payroll Period", "_Test Payroll Period")
|
return frappe.get_doc("Payroll Period", name)
|
||||||
|
|
||||||
def create_exemption_category():
|
def create_exemption_category():
|
||||||
if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"):
|
if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"):
|
||||||
|
@ -3,8 +3,11 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
# import frappe
|
#import frappe
|
||||||
|
import erpnext
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class IncomeTaxSlab(Document):
|
class IncomeTaxSlab(Document):
|
||||||
pass
|
def validate(self):
|
||||||
|
if self.company:
|
||||||
|
self.currency = erpnext.get_company_currency(self.company)
|
||||||
|
@ -17,8 +17,7 @@
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Employee",
|
"label": "Employee",
|
||||||
"options": "Employee",
|
"options": "Employee"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fetch_from": "employee.employee_name",
|
"fetch_from": "employee.employee_name",
|
||||||
@ -52,7 +51,7 @@
|
|||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-30 12:40:07.999878",
|
"modified": "2020-12-17 15:43:29.542977",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Payroll Employee Detail",
|
"name": "Payroll Employee Detail",
|
||||||
|
@ -10,15 +10,22 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
}
|
}
|
||||||
frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet);
|
frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet);
|
||||||
|
|
||||||
frm.set_query("department", function() {
|
frm.events.department_filters(frm);
|
||||||
|
frm.events.payroll_payable_account_filters(frm);
|
||||||
|
},
|
||||||
|
|
||||||
|
department_filters: function (frm) {
|
||||||
|
frm.set_query("department", function () {
|
||||||
return {
|
return {
|
||||||
"filters": {
|
"filters": {
|
||||||
"company": frm.doc.company,
|
"company": frm.doc.company,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
|
||||||
frm.set_query("payroll_payable_account", function() {
|
payroll_payable_account_filters: function (frm) {
|
||||||
|
frm.set_query("payroll_payable_account", function () {
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"company": frm.doc.company,
|
"company": frm.doc.company,
|
||||||
@ -29,12 +36,12 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function (frm) {
|
||||||
if (frm.doc.docstatus == 0) {
|
if (frm.doc.docstatus == 0) {
|
||||||
if(!frm.is_new()) {
|
if (!frm.is_new()) {
|
||||||
frm.page.clear_primary_action();
|
frm.page.clear_primary_action();
|
||||||
frm.add_custom_button(__("Get Employees"),
|
frm.add_custom_button(__("Get Employees"),
|
||||||
function() {
|
function () {
|
||||||
frm.events.get_employee_details(frm);
|
frm.events.get_employee_details(frm);
|
||||||
}
|
}
|
||||||
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
|
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
|
||||||
@ -42,7 +49,7 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
if ((frm.doc.employees || []).length) {
|
if ((frm.doc.employees || []).length) {
|
||||||
frm.page.clear_primary_action();
|
frm.page.clear_primary_action();
|
||||||
frm.page.set_primary_action(__('Create Salary Slips'), () => {
|
frm.page.set_primary_action(__('Create Salary Slips'), () => {
|
||||||
frm.save('Submit').then(()=>{
|
frm.save('Submit').then(() => {
|
||||||
frm.page.clear_primary_action();
|
frm.page.clear_primary_action();
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
frm.events.refresh(frm);
|
frm.events.refresh(frm);
|
||||||
@ -61,48 +68,48 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
method: 'fill_employee_details',
|
method: 'fill_employee_details',
|
||||||
}).then(r => {
|
}).then(r => {
|
||||||
if (r.docs && r.docs[0].employees){
|
if (r.docs && r.docs[0].employees) {
|
||||||
frm.employees = r.docs[0].employees;
|
frm.employees = r.docs[0].employees;
|
||||||
frm.dirty();
|
frm.dirty();
|
||||||
frm.save();
|
frm.save();
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
if(r.docs[0].validate_attendance){
|
if (r.docs[0].validate_attendance) {
|
||||||
render_employee_attendance(frm, r.message);
|
render_employee_attendance(frm, r.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
create_salary_slips: function(frm) {
|
create_salary_slips: function (frm) {
|
||||||
frm.call({
|
frm.call({
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
method: "create_salary_slips",
|
method: "create_salary_slips",
|
||||||
callback: function(r) {
|
callback: function () {
|
||||||
frm.refresh();
|
frm.refresh();
|
||||||
frm.toolbar.refresh();
|
frm.toolbar.refresh();
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
add_context_buttons: function(frm) {
|
add_context_buttons: function (frm) {
|
||||||
if(frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
|
if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
|
||||||
frm.events.add_bank_entry_button(frm);
|
frm.events.add_bank_entry_button(frm);
|
||||||
} else if(frm.doc.salary_slips_created) {
|
} else if (frm.doc.salary_slips_created) {
|
||||||
frm.add_custom_button(__("Submit Salary Slip"), function() {
|
frm.add_custom_button(__("Submit Salary Slip"), function () {
|
||||||
submit_salary_slip(frm);
|
submit_salary_slip(frm);
|
||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
add_bank_entry_button: function(frm) {
|
add_bank_entry_button: function (frm) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries',
|
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries',
|
||||||
args: {
|
args: {
|
||||||
'name': frm.doc.name
|
'name': frm.doc.name
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function (r) {
|
||||||
if (r.message && !r.message.submitted) {
|
if (r.message && !r.message.submitted) {
|
||||||
frm.add_custom_button("Make Bank Entry", function() {
|
frm.add_custom_button("Make Bank Entry", function () {
|
||||||
make_bank_entry(frm);
|
make_bank_entry(frm);
|
||||||
}).addClass("btn-primary");
|
}).addClass("btn-primary");
|
||||||
}
|
}
|
||||||
@ -141,8 +148,37 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
payroll_frequency: function (frm) {
|
payroll_frequency: function (frm) {
|
||||||
frm.trigger("set_start_end_dates");
|
frm.trigger("set_start_end_dates").then( ()=> {
|
||||||
frm.events.clear_employee_table(frm);
|
frm.events.clear_employee_table(frm);
|
||||||
|
frm.events.get_employee_with_salary_slip_and_set_query(frm);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
employee_filters: function (frm, emp_list) {
|
||||||
|
frm.set_query('employee', 'employees', () => {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
name: ["not in", emp_list]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
get_employee_with_salary_slip_and_set_query: function (frm) {
|
||||||
|
frappe.db.get_list('Salary Slip', {
|
||||||
|
filters: {
|
||||||
|
start_date: frm.doc.start_date,
|
||||||
|
end_date: frm.doc.end_date,
|
||||||
|
docstatus: 1,
|
||||||
|
},
|
||||||
|
fields: ['employee']
|
||||||
|
}).then((emp) => {
|
||||||
|
var emp_list = [];
|
||||||
|
emp.forEach((employee_data) => {
|
||||||
|
emp_list.push(Object.values(employee_data)[0]);
|
||||||
|
});
|
||||||
|
frm.events.employee_filters(frm, emp_list);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
company: function (frm) {
|
company: function (frm) {
|
||||||
@ -164,17 +200,17 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
from_currency: frm.doc.currency,
|
from_currency: frm.doc.currency,
|
||||||
to_currency: company_currency,
|
to_currency: company_currency,
|
||||||
},
|
},
|
||||||
callback: function(r) {
|
callback: function (r) {
|
||||||
frm.set_value("exchange_rate", flt(r.message));
|
frm.set_value("exchange_rate", flt(r.message));
|
||||||
frm.set_df_property('exchange_rate', 'hidden', 0);
|
frm.set_df_property('exchange_rate', 'hidden', 0);
|
||||||
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
|
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency +
|
||||||
+ " = [?] " + company_currency);
|
" = [?] " + company_currency);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
frm.set_value("exchange_rate", 1.0);
|
frm.set_value("exchange_rate", 1.0);
|
||||||
frm.set_df_property('exchange_rate', 'hidden', 1);
|
frm.set_df_property('exchange_rate', 'hidden', 1);
|
||||||
frm.set_df_property("exchange_rate", "description", "" );
|
frm.set_df_property("exchange_rate", "description", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -192,9 +228,9 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
start_date: function (frm) {
|
start_date: function (frm) {
|
||||||
if(!in_progress && frm.doc.start_date){
|
if (!in_progress && frm.doc.start_date) {
|
||||||
frm.trigger("set_end_date");
|
frm.trigger("set_end_date");
|
||||||
}else{
|
} else {
|
||||||
// reset flag
|
// reset flag
|
||||||
in_progress = false;
|
in_progress = false;
|
||||||
}
|
}
|
||||||
@ -228,7 +264,7 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
set_end_date: function(frm){
|
set_end_date: function (frm) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
|
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
|
||||||
args: {
|
args: {
|
||||||
@ -243,19 +279,19 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
validate_attendance: function(frm){
|
validate_attendance: function (frm) {
|
||||||
if(frm.doc.validate_attendance && frm.doc.employees){
|
if (frm.doc.validate_attendance && frm.doc.employees) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'validate_employee_attendance',
|
method: 'validate_employee_attendance',
|
||||||
args: {},
|
args: {},
|
||||||
callback: function(r) {
|
callback: function (r) {
|
||||||
render_employee_attendance(frm, r.message);
|
render_employee_attendance(frm, r.message);
|
||||||
},
|
},
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
freeze: true,
|
freeze: true,
|
||||||
freeze_message: __('Validating Employee Attendance...')
|
freeze_message: __('Validating Employee Attendance...')
|
||||||
});
|
});
|
||||||
}else{
|
} else {
|
||||||
frm.fields_dict.attendance_detail_html.html("");
|
frm.fields_dict.attendance_detail_html.html("");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -270,18 +306,20 @@ frappe.ui.form.on('Payroll Entry', {
|
|||||||
|
|
||||||
const submit_salary_slip = function (frm) {
|
const submit_salary_slip = function (frm) {
|
||||||
frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'),
|
frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'),
|
||||||
function() {
|
function () {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
method: 'submit_salary_slips',
|
method: 'submit_salary_slips',
|
||||||
args: {},
|
args: {},
|
||||||
callback: function() {frm.events.refresh(frm);},
|
callback: function () {
|
||||||
|
frm.events.refresh(frm);
|
||||||
|
},
|
||||||
doc: frm.doc,
|
doc: frm.doc,
|
||||||
freeze: true,
|
freeze: true,
|
||||||
freeze_message: __('Submitting Salary Slips and creating Journal Entry...')
|
freeze_message: __('Submitting Salary Slips and creating Journal Entry...')
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
function() {
|
function () {
|
||||||
if(frappe.dom.freeze_count) {
|
if (frappe.dom.freeze_count) {
|
||||||
frappe.dom.unfreeze();
|
frappe.dom.unfreeze();
|
||||||
frm.events.refresh(frm);
|
frm.events.refresh(frm);
|
||||||
}
|
}
|
||||||
@ -295,9 +333,11 @@ let make_bank_entry = function (frm) {
|
|||||||
return frappe.call({
|
return frappe.call({
|
||||||
doc: cur_frm.doc,
|
doc: cur_frm.doc,
|
||||||
method: "make_payment_entry",
|
method: "make_payment_entry",
|
||||||
callback: function() {
|
callback: function () {
|
||||||
frappe.set_route(
|
frappe.set_route(
|
||||||
'List', 'Journal Entry', {"Journal Entry Account.reference_name": frm.doc.name}
|
'List', 'Journal Entry', {
|
||||||
|
"Journal Entry Account.reference_name": frm.doc.name
|
||||||
|
}
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
freeze: true,
|
freeze: true,
|
||||||
@ -309,11 +349,19 @@ let make_bank_entry = function (frm) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let render_employee_attendance = function (frm, data) {
|
||||||
let render_employee_attendance = function(frm, data) {
|
|
||||||
frm.fields_dict.attendance_detail_html.html(
|
frm.fields_dict.attendance_detail_html.html(
|
||||||
frappe.render_template('employees_to_mark_attendance', {
|
frappe.render_template('employees_to_mark_attendance', {
|
||||||
data: data
|
data: data
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
frappe.ui.form.on('Payroll Employee Detail', {
|
||||||
|
employee: function(frm) {
|
||||||
|
frm.events.clear_employee_table(frm);
|
||||||
|
if (!frm.doc.payroll_frequency) {
|
||||||
|
frappe.throw(__("Please set a Payroll Frequency"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@ -129,8 +129,7 @@
|
|||||||
"fieldname": "employees",
|
"fieldname": "employees",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"label": "Employee Details",
|
"label": "Employee Details",
|
||||||
"options": "Payroll Employee Detail",
|
"options": "Payroll Employee Detail"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "section_break_13",
|
"fieldname": "section_break_13",
|
||||||
@ -290,7 +289,7 @@
|
|||||||
"icon": "fa fa-cog",
|
"icon": "fa fa-cog",
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-23 13:00:33.753228",
|
"modified": "2020-12-17 15:13:17.766210",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Payroll Entry",
|
"name": "Payroll Entry",
|
||||||
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe, erpnext
|
import frappe, erpnext
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from dateutil.relativedelta import relativedelta
|
from dateutil.relativedelta import relativedelta
|
||||||
from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff
|
from frappe.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT, date_diff, comma_and
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
||||||
@ -19,16 +19,26 @@ class PayrollEntry(Document):
|
|||||||
# check if salary slips were manually submitted
|
# check if salary slips were manually submitted
|
||||||
entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name'])
|
entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name'])
|
||||||
if cint(entries) == len(self.employees):
|
if cint(entries) == len(self.employees):
|
||||||
self.set_onload("submitted_ss", True)
|
self.set_onload("submitted_ss", True)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.create_salary_slips()
|
self.create_salary_slips()
|
||||||
|
|
||||||
def before_submit(self):
|
def before_submit(self):
|
||||||
|
self.validate_employee_details()
|
||||||
if self.validate_attendance:
|
if self.validate_attendance:
|
||||||
if self.validate_employee_attendance():
|
if self.validate_employee_attendance():
|
||||||
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
|
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
|
||||||
|
|
||||||
|
def validate_employee_details(self):
|
||||||
|
emp_with_sal_slip = []
|
||||||
|
for employee_details in self.employees:
|
||||||
|
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
|
||||||
|
emp_with_sal_slip.append(employee_details.employee)
|
||||||
|
|
||||||
|
if len(emp_with_sal_slip):
|
||||||
|
frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip)))
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||||
where payroll_entry=%s """, (self.name)))
|
where payroll_entry=%s """, (self.name)))
|
||||||
@ -71,8 +81,17 @@ class PayrollEntry(Document):
|
|||||||
and t2.docstatus = 1
|
and t2.docstatus = 1
|
||||||
%s order by t2.from_date desc
|
%s order by t2.from_date desc
|
||||||
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True)
|
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True)
|
||||||
|
|
||||||
|
emp_list = self.remove_payrolled_employees(emp_list)
|
||||||
return emp_list
|
return emp_list
|
||||||
|
|
||||||
|
def remove_payrolled_employees(self, emp_list):
|
||||||
|
for employee_details in emp_list:
|
||||||
|
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
|
||||||
|
emp_list.remove(employee_details)
|
||||||
|
|
||||||
|
return emp_list
|
||||||
|
|
||||||
def fill_employee_details(self):
|
def fill_employee_details(self):
|
||||||
self.set('employees', [])
|
self.set('employees', [])
|
||||||
employees = self.get_emp_list()
|
employees = self.get_emp_list()
|
||||||
|
@ -22,7 +22,7 @@ class TestPayrollEntry(unittest.TestCase):
|
|||||||
frappe.db.sql("delete from `tab%s`" % dt)
|
frappe.db.sql("delete from `tab%s`" % dt)
|
||||||
|
|
||||||
make_earning_salary_component(setup=True, company_list=["_Test Company"])
|
make_earning_salary_component(setup=True, company_list=["_Test Company"])
|
||||||
make_deduction_salary_component(setup=True, company_list=["_Test Company"])
|
make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
|
||||||
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
|
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
|
||||||
|
|
||||||
@ -107,9 +107,9 @@ class TestPayrollEntry(unittest.TestCase):
|
|||||||
frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
|
frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
|
||||||
frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account",
|
frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account",
|
||||||
"_Test Payroll Payable - _TC")
|
"_Test Payroll Payable - _TC")
|
||||||
|
currency=frappe.db.get_value("Company", "_Test Company", "default_currency")
|
||||||
make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency"))
|
make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False)
|
||||||
make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency"))
|
make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
|
||||||
|
|
||||||
dates = get_start_end_dates('Monthly', nowdate())
|
dates = get_start_end_dates('Monthly', nowdate())
|
||||||
if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}):
|
if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}):
|
||||||
|
@ -4,9 +4,13 @@
|
|||||||
frappe.ui.form.on('Retention Bonus', {
|
frappe.ui.form.on('Retention Bonus', {
|
||||||
setup: function(frm) {
|
setup: function(frm) {
|
||||||
frm.set_query("employee", function() {
|
frm.set_query("employee", function() {
|
||||||
|
if (!frm.doc.company) {
|
||||||
|
frappe.msgprint(__("Please Select Company First"));
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
filters: {
|
filters: {
|
||||||
"status": "Active"
|
"status": "Active",
|
||||||
|
"company": frm.doc.company
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -125,15 +125,15 @@ frappe.ui.form.on("Salary Slip", {
|
|||||||
|
|
||||||
change_form_labels: function(frm, company_currency) {
|
change_form_labels: function(frm, company_currency) {
|
||||||
frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction",
|
frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction",
|
||||||
"base_net_pay", "base_rounded_total", "base_total_in_words"],
|
"base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
|
||||||
company_currency);
|
company_currency);
|
||||||
|
|
||||||
frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"],
|
frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date"],
|
||||||
frm.doc.currency);
|
frm.doc.currency);
|
||||||
|
|
||||||
// toggle fields
|
// toggle fields
|
||||||
frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction",
|
frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction",
|
||||||
"base_net_pay", "base_rounded_total", "base_total_in_words"],
|
"base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
|
||||||
frm.doc.currency != company_currency);
|
frm.doc.currency != company_currency);
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -214,14 +214,16 @@ frappe.ui.form.on('Salary Slip Timesheet', {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var calculate_totals = function(frm) {
|
var calculate_totals = function(frm) {
|
||||||
if (frm.doc.earnings || frm.doc.deductions) {
|
if (frm.doc.docstatus === 0) {
|
||||||
frappe.call({
|
if (frm.doc.earnings || frm.doc.deductions) {
|
||||||
method: "set_totals",
|
frappe.call({
|
||||||
doc: frm.doc,
|
method: "set_totals",
|
||||||
callback: function() {
|
doc: frm.doc,
|
||||||
frm.refresh_fields();
|
callback: function() {
|
||||||
}
|
frm.refresh_fields();
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,9 +69,13 @@
|
|||||||
"net_pay_info",
|
"net_pay_info",
|
||||||
"net_pay",
|
"net_pay",
|
||||||
"base_net_pay",
|
"base_net_pay",
|
||||||
|
"year_to_date",
|
||||||
|
"base_year_to_date",
|
||||||
"column_break_53",
|
"column_break_53",
|
||||||
"rounded_total",
|
"rounded_total",
|
||||||
"base_rounded_total",
|
"base_rounded_total",
|
||||||
|
"month_to_date",
|
||||||
|
"base_month_to_date",
|
||||||
"section_break_55",
|
"section_break_55",
|
||||||
"total_in_words",
|
"total_in_words",
|
||||||
"column_break_69",
|
"column_break_69",
|
||||||
@ -578,13 +582,41 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_69",
|
"fieldname": "column_break_69",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "year_to_date",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Year To Date",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "month_to_date",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Month To Date",
|
||||||
|
"options": "currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_year_to_date",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Year To Date(Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "base_month_to_date",
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"label": "Month To Date(Company Currency)",
|
||||||
|
"options": "Company:company:default_currency",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-file-text",
|
"icon": "fa fa-file-text",
|
||||||
"idx": 9,
|
"idx": 9,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-21 23:02:59.400249",
|
"modified": "2020-12-21 23:43:44.959840",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Salary Slip",
|
"name": "Salary Slip",
|
||||||
|
@ -5,7 +5,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe, erpnext
|
import frappe, erpnext
|
||||||
import datetime, math
|
import datetime, math
|
||||||
|
|
||||||
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate
|
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
|
||||||
from frappe.model.naming import make_autoname
|
from frappe.model.naming import make_autoname
|
||||||
|
|
||||||
from frappe import msgprint, _
|
from frappe import msgprint, _
|
||||||
@ -18,6 +18,7 @@ from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_fac
|
|||||||
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
|
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
|
||||||
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
|
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
|
||||||
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
|
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
|
||||||
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
|
|
||||||
class SalarySlip(TransactionBase):
|
class SalarySlip(TransactionBase):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
@ -49,6 +50,8 @@ class SalarySlip(TransactionBase):
|
|||||||
self.get_working_days_details(lwp = self.leave_without_pay)
|
self.get_working_days_details(lwp = self.leave_without_pay)
|
||||||
|
|
||||||
self.calculate_net_pay()
|
self.calculate_net_pay()
|
||||||
|
self.compute_year_to_date()
|
||||||
|
self.compute_month_to_date()
|
||||||
|
|
||||||
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
|
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
|
||||||
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
|
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
|
||||||
@ -1125,6 +1128,46 @@ class SalarySlip(TransactionBase):
|
|||||||
self.gross_pay += self.earnings[i].amount
|
self.gross_pay += self.earnings[i].amount
|
||||||
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
|
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
|
||||||
|
|
||||||
|
def compute_year_to_date(self):
|
||||||
|
year_to_date = 0
|
||||||
|
payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
|
||||||
|
|
||||||
|
if payroll_period:
|
||||||
|
period_start_date = payroll_period.start_date
|
||||||
|
period_end_date = payroll_period.end_date
|
||||||
|
else:
|
||||||
|
# get dates based on fiscal year if no payroll period exists
|
||||||
|
fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1)
|
||||||
|
period_start_date = fiscal_year.year_start_date
|
||||||
|
period_end_date = fiscal_year.year_end_date
|
||||||
|
|
||||||
|
salary_slip_sum = frappe.get_list('Salary Slip',
|
||||||
|
fields = ['sum(net_pay) as sum'],
|
||||||
|
filters = {'employee_name' : self.employee_name,
|
||||||
|
'start_date' : ['>=', period_start_date],
|
||||||
|
'end_date' : ['<', period_end_date]})
|
||||||
|
|
||||||
|
|
||||||
|
year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
|
||||||
|
|
||||||
|
year_to_date += self.net_pay
|
||||||
|
self.year_to_date = year_to_date
|
||||||
|
|
||||||
|
def compute_month_to_date(self):
|
||||||
|
month_to_date = 0
|
||||||
|
first_day_of_the_month = get_first_day(self.start_date)
|
||||||
|
salary_slip_sum = frappe.get_list('Salary Slip',
|
||||||
|
fields = ['sum(net_pay) as sum'],
|
||||||
|
filters = {'employee_name' : self.employee_name,
|
||||||
|
'start_date' : ['>=', first_day_of_the_month],
|
||||||
|
'end_date' : ['<', self.start_date]
|
||||||
|
})
|
||||||
|
|
||||||
|
month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
|
||||||
|
|
||||||
|
month_to_date += self.net_pay
|
||||||
|
self.month_to_date = month_to_date
|
||||||
|
|
||||||
def unlink_ref_doc_from_salary_slip(ref_no):
|
def unlink_ref_doc_from_salary_slip(ref_no):
|
||||||
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
|
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
|
||||||
where journal_entry=%s and docstatus < 2""", (ref_no))
|
where journal_entry=%s and docstatus < 2""", (ref_no))
|
||||||
|
@ -9,7 +9,7 @@ import calendar
|
|||||||
import random
|
import random
|
||||||
from erpnext.accounts.utils import get_fiscal_year
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
from frappe.utils.make_random import get_random
|
from frappe.utils.make_random import get_random
|
||||||
from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day
|
from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr
|
||||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||||
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
|
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
|
||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
@ -240,7 +240,11 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
interest_income_account='Interest Income Account - _TC',
|
interest_income_account='Interest Income Account - _TC',
|
||||||
penalty_income_account='Penalty Income Account - _TC')
|
penalty_income_account='Penalty Income Account - _TC')
|
||||||
|
|
||||||
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR')
|
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
|
||||||
|
|
||||||
|
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
|
||||||
|
payroll_period=payroll_period)
|
||||||
|
|
||||||
frappe.db.sql("""delete from `tabLoan""")
|
frappe.db.sql("""delete from `tabLoan""")
|
||||||
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
|
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
|
||||||
loan.repay_from_salary = 1
|
loan.repay_from_salary = 1
|
||||||
@ -290,6 +294,33 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
self.assertEqual(salary_slip.gross_pay, 78000)
|
self.assertEqual(salary_slip.gross_pay, 78000)
|
||||||
self.assertEqual(salary_slip.base_gross_pay, 78000*70)
|
self.assertEqual(salary_slip.base_gross_pay, 78000*70)
|
||||||
|
|
||||||
|
def test_year_to_date_computation(self):
|
||||||
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||||
|
|
||||||
|
applicant = make_employee("test_ytd@salary.com", company="_Test Company")
|
||||||
|
|
||||||
|
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
|
||||||
|
|
||||||
|
create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"),
|
||||||
|
company="_Test Company")
|
||||||
|
|
||||||
|
salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD",
|
||||||
|
"Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period)
|
||||||
|
|
||||||
|
# clear salary slip for this employee
|
||||||
|
frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
|
||||||
|
|
||||||
|
create_salary_slips_for_payroll_period(applicant, salary_structure.name,
|
||||||
|
payroll_period, deduct_random=False)
|
||||||
|
|
||||||
|
salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name':
|
||||||
|
'test_ytd@salary.com'}, order_by = 'posting_date')
|
||||||
|
|
||||||
|
year_to_date = 0
|
||||||
|
for slip in salary_slips:
|
||||||
|
year_to_date += slip.net_pay
|
||||||
|
self.assertEqual(slip.year_to_date, year_to_date)
|
||||||
|
|
||||||
def test_tax_for_payroll_period(self):
|
def test_tax_for_payroll_period(self):
|
||||||
data = {}
|
data = {}
|
||||||
# test the impact of tax exemption declaration, tax exemption proof submission
|
# test the impact of tax exemption declaration, tax exemption proof submission
|
||||||
@ -410,10 +441,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
|||||||
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
|
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
|
||||||
|
|
||||||
employee = frappe.db.get_value("Employee", {"user_id": user})
|
employee = frappe.db.get_value("Employee", {"user_id": user})
|
||||||
if not frappe.db.exists('Salary Structure', salary_structure):
|
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
|
||||||
salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee)
|
|
||||||
else:
|
|
||||||
salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure)
|
|
||||||
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
|
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
|
||||||
|
|
||||||
if not salary_slip_name:
|
if not salary_slip_name:
|
||||||
@ -557,14 +585,6 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No
|
|||||||
"amount": 200,
|
"amount": 200,
|
||||||
"exempted_from_income_tax": 1
|
"exempted_from_income_tax": 1
|
||||||
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"salary_component": 'TDS',
|
|
||||||
"abbr":'T',
|
|
||||||
"type": "Deduction",
|
|
||||||
"depends_on_payment_days": 0,
|
|
||||||
"variable_based_on_taxable_salary": 1,
|
|
||||||
"round_to_the_nearest_integer": 1
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
if not test_tax:
|
if not test_tax:
|
||||||
@ -575,6 +595,15 @@ def make_deduction_salary_component(setup=False, test_tax=False, company_list=No
|
|||||||
"type": "Deduction",
|
"type": "Deduction",
|
||||||
"round_to_the_nearest_integer": 1
|
"round_to_the_nearest_integer": 1
|
||||||
})
|
})
|
||||||
|
else:
|
||||||
|
data.append({
|
||||||
|
"salary_component": 'TDS',
|
||||||
|
"abbr":'T',
|
||||||
|
"type": "Deduction",
|
||||||
|
"depends_on_payment_days": 0,
|
||||||
|
"variable_based_on_taxable_salary": 1,
|
||||||
|
"round_to_the_nearest_integer": 1
|
||||||
|
})
|
||||||
if setup or test_tax:
|
if setup or test_tax:
|
||||||
make_salary_component(data, test_tax, company_list)
|
make_salary_component(data, test_tax, company_list)
|
||||||
|
|
||||||
@ -631,8 +660,13 @@ def create_benefit_claim(employee, payroll_period, amount, component):
|
|||||||
}).submit()
|
}).submit()
|
||||||
return claim_date
|
return claim_date
|
||||||
|
|
||||||
def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()):
|
def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None,
|
||||||
frappe.db.sql("""delete from `tabIncome Tax Slab`""")
|
company=None):
|
||||||
|
if not currency:
|
||||||
|
currency = erpnext.get_default_currency()
|
||||||
|
|
||||||
|
if company:
|
||||||
|
currency = erpnext.get_company_currency(company)
|
||||||
|
|
||||||
slabs = [
|
slabs = [
|
||||||
{
|
{
|
||||||
@ -652,26 +686,33 @@ def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption =
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
income_tax_slab = frappe.new_doc("Income Tax Slab")
|
income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency})
|
||||||
income_tax_slab.name = "Tax Slab: " + payroll_period.name
|
if not income_tax_slab_name:
|
||||||
income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2)
|
income_tax_slab = frappe.new_doc("Income Tax Slab")
|
||||||
income_tax_slab.currency = currency
|
income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency)
|
||||||
|
income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2)
|
||||||
|
income_tax_slab.company = company or ''
|
||||||
|
income_tax_slab.currency = currency
|
||||||
|
|
||||||
if allow_tax_exemption:
|
if allow_tax_exemption:
|
||||||
income_tax_slab.allow_tax_exemption = 1
|
income_tax_slab.allow_tax_exemption = 1
|
||||||
income_tax_slab.standard_tax_exemption_amount = 50000
|
income_tax_slab.standard_tax_exemption_amount = 50000
|
||||||
|
|
||||||
for item in slabs:
|
for item in slabs:
|
||||||
income_tax_slab.append("slabs", item)
|
income_tax_slab.append("slabs", item)
|
||||||
|
|
||||||
income_tax_slab.append("other_taxes_and_charges", {
|
income_tax_slab.append("other_taxes_and_charges", {
|
||||||
"description": "cess",
|
"description": "cess",
|
||||||
"percent": 4
|
"percent": 4
|
||||||
})
|
})
|
||||||
|
|
||||||
income_tax_slab.save()
|
income_tax_slab.save()
|
||||||
if not dont_submit:
|
if not dont_submit:
|
||||||
income_tax_slab.submit()
|
income_tax_slab.submit()
|
||||||
|
|
||||||
|
return income_tax_slab.name
|
||||||
|
else:
|
||||||
|
return income_tax_slab_name
|
||||||
|
|
||||||
def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True):
|
def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True):
|
||||||
deducted_dates = []
|
deducted_dates = []
|
||||||
|
@ -55,17 +55,17 @@ frappe.ui.form.on('Salary Structure', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
set_earning_deduction_component: function(frm) {
|
set_earning_deduction_component: function(frm) {
|
||||||
if(!frm.doc.currency && !frm.doc.company) return;
|
if(!frm.doc.company) return;
|
||||||
frm.set_query("salary_component", "earnings", function() {
|
frm.set_query("salary_component", "earnings", function() {
|
||||||
return {
|
return {
|
||||||
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
|
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
|
||||||
filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company}
|
filters: {type: "earning", company: frm.doc.company}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
frm.set_query("salary_component", "deductions", function() {
|
frm.set_query("salary_component", "deductions", function() {
|
||||||
return {
|
return {
|
||||||
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
|
query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
|
||||||
filters: {type: "deduction", currency: frm.doc.currency, company: frm.doc.company}
|
filters: {type: "deduction", company: frm.doc.company}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -74,7 +74,6 @@ frappe.ui.form.on('Salary Structure', {
|
|||||||
currency: function(frm) {
|
currency: function(frm) {
|
||||||
calculate_totals(frm.doc);
|
calculate_totals(frm.doc);
|
||||||
frm.trigger("set_dynamic_labels")
|
frm.trigger("set_dynamic_labels")
|
||||||
frm.trigger('set_earning_deduction_component');
|
|
||||||
frm.refresh()
|
frm.refresh()
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -210,7 +210,7 @@ def get_employees(salary_structure):
|
|||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters):
|
def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters):
|
||||||
if len(filters) < 3:
|
if len(filters) < 2:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
return frappe.db.sql("""
|
return frappe.db.sql("""
|
||||||
|
@ -114,7 +114,7 @@ class TestSalaryStructure(unittest.TestCase):
|
|||||||
self.assertEqual(sal_struct.currency, 'USD')
|
self.assertEqual(sal_struct.currency, 'USD')
|
||||||
|
|
||||||
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None,
|
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None,
|
||||||
test_tax=False, company=None, currency=erpnext.get_default_currency()):
|
test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None):
|
||||||
if test_tax:
|
if test_tax:
|
||||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
|
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
|
||||||
|
|
||||||
@ -141,16 +141,24 @@ def make_salary_structure(salary_structure, payroll_frequency, employee=None, do
|
|||||||
|
|
||||||
if employee and not frappe.db.get_value("Salary Structure Assignment",
|
if employee and not frappe.db.get_value("Salary Structure Assignment",
|
||||||
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1:
|
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1:
|
||||||
create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency)
|
create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency,
|
||||||
|
payroll_period=payroll_period)
|
||||||
|
|
||||||
return salary_structure_doc
|
return salary_structure_doc
|
||||||
|
|
||||||
def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()):
|
def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(),
|
||||||
|
payroll_period=None):
|
||||||
|
|
||||||
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
|
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
|
||||||
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
|
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
|
||||||
|
|
||||||
payroll_period = create_payroll_period()
|
if not payroll_period:
|
||||||
create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency)
|
payroll_period = create_payroll_period()
|
||||||
|
|
||||||
|
income_tax_slab = frappe.db.get_value("Income Tax Slab", {"currency": currency})
|
||||||
|
|
||||||
|
if not income_tax_slab:
|
||||||
|
income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency)
|
||||||
|
|
||||||
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
|
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
|
||||||
salary_structure_assignment.employee = employee
|
salary_structure_assignment.employee = employee
|
||||||
@ -162,7 +170,7 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non
|
|||||||
salary_structure_assignment.payroll_payable_account = get_payable_account(company)
|
salary_structure_assignment.payroll_payable_account = get_payable_account(company)
|
||||||
salary_structure_assignment.company = company or erpnext.get_default_company()
|
salary_structure_assignment.company = company or erpnext.get_default_company()
|
||||||
salary_structure_assignment.save(ignore_permissions=True)
|
salary_structure_assignment.save(ignore_permissions=True)
|
||||||
salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period"
|
salary_structure_assignment.income_tax_slab = income_tax_slab
|
||||||
salary_structure_assignment.submit()
|
salary_structure_assignment.submit()
|
||||||
return salary_structure_assignment
|
return salary_structure_assignment
|
||||||
|
|
||||||
|
@ -43,7 +43,7 @@ class SalaryStructureAssignment(Document):
|
|||||||
|
|
||||||
def set_payroll_payable_account(self):
|
def set_payroll_payable_account(self):
|
||||||
if not self.payroll_payable_account:
|
if not self.payroll_payable_account:
|
||||||
payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account')
|
payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account')
|
||||||
if not payroll_payable_account:
|
if not payroll_payable_account:
|
||||||
payroll_payable_account = frappe.db.get_value(
|
payroll_payable_account = frappe.db.get_value(
|
||||||
"Account", {
|
"Account", {
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('E Invoice Request Log', {
|
||||||
|
// refresh: function(frm) {
|
||||||
|
|
||||||
|
// }
|
||||||
|
});
|
@ -0,0 +1,103 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "EINV-REQ-.#####",
|
||||||
|
"creation": "2020-12-08 12:54:08.175992",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"user",
|
||||||
|
"url",
|
||||||
|
"headers",
|
||||||
|
"response",
|
||||||
|
"column_break_7",
|
||||||
|
"timestamp",
|
||||||
|
"reference_invoice",
|
||||||
|
"data"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "user",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "User",
|
||||||
|
"options": "User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "reference_invoice",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Reference Invoice",
|
||||||
|
"options": "Sales Invoice"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "headers",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Headers",
|
||||||
|
"options": "JSON"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "data",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Data",
|
||||||
|
"options": "JSON"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "Now",
|
||||||
|
"fieldname": "timestamp",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"label": "Timestamp"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "response",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Response",
|
||||||
|
"options": "JSON"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "url",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "URL"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_7",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-12-24 21:09:38.882866",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Regional",
|
||||||
|
"name": "E Invoice Request Log",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "System Manager",
|
||||||
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC"
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class EInvoiceRequestLog(Document):
|
||||||
|
pass
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestEInvoiceRequestLog(unittest.TestCase):
|
||||||
|
pass
|
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
|
||||||
|
frappe.ui.form.on('E Invoice Settings', {
|
||||||
|
refresh(frm) {
|
||||||
|
const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing';
|
||||||
|
frm.dashboard.set_headline(
|
||||||
|
__("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
@ -0,0 +1,58 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-09-24 16:23:16.235722",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"enable",
|
||||||
|
"section_break_2",
|
||||||
|
"credentials",
|
||||||
|
"auth_token",
|
||||||
|
"token_expiry"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"fieldname": "enable",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"label": "Enable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"depends_on": "enable",
|
||||||
|
"fieldname": "section_break_2",
|
||||||
|
"fieldtype": "Section Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "auth_token",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"hidden": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "token_expiry",
|
||||||
|
"fieldtype": "Datetime",
|
||||||
|
"hidden": 1,
|
||||||
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "credentials",
|
||||||
|
"fieldtype": "Table",
|
||||||
|
"label": "Credentials",
|
||||||
|
"mandatory_depends_on": "enable",
|
||||||
|
"options": "E Invoice User"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"issingle": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-12-22 15:34:57.280044",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Regional",
|
||||||
|
"name": "E Invoice Settings",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,14 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class EInvoiceSettings(Document):
|
||||||
|
def validate(self):
|
||||||
|
if self.enable and not self.credentials:
|
||||||
|
frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.'))
|
||||||
|
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
class TestEInvoiceSettings(unittest.TestCase):
|
||||||
|
pass
|
0
erpnext/regional/doctype/e_invoice_user/__init__.py
Normal file
0
erpnext/regional/doctype/e_invoice_user/__init__.py
Normal file
48
erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
Normal file
48
erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-12-22 15:02:46.229474",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"gstin",
|
||||||
|
"username",
|
||||||
|
"password"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "gstin",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "GSTIN",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "username",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Username",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "password",
|
||||||
|
"fieldtype": "Password",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Password",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-12-22 15:10:53.466205",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Regional",
|
||||||
|
"name": "E Invoice User",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
10
erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
Normal file
10
erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class EInvoiceUser(Document):
|
||||||
|
pass
|
@ -29,25 +29,12 @@
|
|||||||
],
|
],
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-09-30 20:08:18.764798",
|
"modified": "2020-12-25 20:20:22.342426",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Regional",
|
"module": "Regional",
|
||||||
"name": "UAE VAT Settings",
|
"name": "UAE VAT Settings",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [],
|
||||||
{
|
|
||||||
"create": 1,
|
|
||||||
"delete": 1,
|
|
||||||
"email": 1,
|
|
||||||
"export": 1,
|
|
||||||
"print": 1,
|
|
||||||
"read": 1,
|
|
||||||
"report": 1,
|
|
||||||
"role": "System Manager",
|
|
||||||
"share": 1,
|
|
||||||
"write": 1
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
0
erpnext/regional/india/e_invoice/__init__.py
Normal file
0
erpnext/regional/india/e_invoice/__init__.py
Normal file
31
erpnext/regional/india/e_invoice/einv_item_template.json
Normal file
31
erpnext/regional/india/e_invoice/einv_item_template.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{{
|
||||||
|
"SlNo": "{item.sr_no}",
|
||||||
|
"PrdDesc": "{item.description}",
|
||||||
|
"IsServc": "{item.is_service_item}",
|
||||||
|
"HsnCd": "{item.gst_hsn_code}",
|
||||||
|
"Barcde": "{item.barcode}",
|
||||||
|
"Unit": "{item.uom}",
|
||||||
|
"Qty": "{item.qty}",
|
||||||
|
"FreeQty": "{item.free_qty}",
|
||||||
|
"UnitPrice": "{item.unit_rate}",
|
||||||
|
"TotAmt": "{item.gross_amount}",
|
||||||
|
"Discount": "{item.discount_amount}",
|
||||||
|
"AssAmt": "{item.taxable_value}",
|
||||||
|
"PrdSlNo": "{item.serial_no}",
|
||||||
|
"GstRt": "{item.tax_rate}",
|
||||||
|
"IgstAmt": "{item.igst_amount}",
|
||||||
|
"CgstAmt": "{item.cgst_amount}",
|
||||||
|
"SgstAmt": "{item.sgst_amount}",
|
||||||
|
"CesRt": "{item.cess_rate}",
|
||||||
|
"CesAmt": "{item.cess_amount}",
|
||||||
|
"CesNonAdvlAmt": "{item.cess_nadv_amount}",
|
||||||
|
"StateCesRt": "{item.state_cess_rate}",
|
||||||
|
"StateCesAmt": "{item.state_cess_amount}",
|
||||||
|
"StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
|
||||||
|
"OthChrg": "{item.other_charges}",
|
||||||
|
"TotItemVal": "{item.total_value}",
|
||||||
|
"BchDtls": {{
|
||||||
|
"Nm": "{item.batch_no}",
|
||||||
|
"ExpDt": "{item.batch_expiry_date}"
|
||||||
|
}}
|
||||||
|
}}
|
110
erpnext/regional/india/e_invoice/einv_template.json
Normal file
110
erpnext/regional/india/e_invoice/einv_template.json
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
{{
|
||||||
|
"Version": "1.1",
|
||||||
|
"TranDtls": {{
|
||||||
|
"TaxSch": "{transaction_details.tax_scheme}",
|
||||||
|
"SupTyp": "{transaction_details.supply_type}",
|
||||||
|
"RegRev": "{transaction_details.reverse_charge}",
|
||||||
|
"EcmGstin": "{transaction_details.ecom_gstin}",
|
||||||
|
"IgstOnIntra": "{transaction_details.igst_on_intra}"
|
||||||
|
}},
|
||||||
|
"DocDtls": {{
|
||||||
|
"Typ": "{doc_details.invoice_type}",
|
||||||
|
"No": "{doc_details.invoice_name}",
|
||||||
|
"Dt": "{doc_details.invoice_date}"
|
||||||
|
}},
|
||||||
|
"SellerDtls": {{
|
||||||
|
"Gstin": "{seller_details.gstin}",
|
||||||
|
"LglNm": "{seller_details.legal_name}",
|
||||||
|
"TrdNm": "{seller_details.trade_name}",
|
||||||
|
"Loc": "{seller_details.location}",
|
||||||
|
"Pin": "{seller_details.pincode}",
|
||||||
|
"Stcd": "{seller_details.state_code}",
|
||||||
|
"Addr1": "{seller_details.address_line1}",
|
||||||
|
"Addr2": "{seller_details.address_line2}",
|
||||||
|
"Ph": "{seller_details.phone}",
|
||||||
|
"Em": "{seller_details.email}"
|
||||||
|
}},
|
||||||
|
"BuyerDtls": {{
|
||||||
|
"Gstin": "{buyer_details.gstin}",
|
||||||
|
"LglNm": "{buyer_details.legal_name}",
|
||||||
|
"TrdNm": "{buyer_details.trade_name}",
|
||||||
|
"Addr1": "{buyer_details.address_line1}",
|
||||||
|
"Addr2": "{buyer_details.address_line2}",
|
||||||
|
"Loc": "{buyer_details.location}",
|
||||||
|
"Pin": "{buyer_details.pincode}",
|
||||||
|
"Stcd": "{buyer_details.state_code}",
|
||||||
|
"Ph": "{buyer_details.phone}",
|
||||||
|
"Em": "{buyer_details.email}",
|
||||||
|
"Pos": "{buyer_details.place_of_supply}"
|
||||||
|
}},
|
||||||
|
"DispDtls": {{
|
||||||
|
"Nm": "{dispatch_details.company_name}",
|
||||||
|
"Addr1": "{dispatch_details.address_line1}",
|
||||||
|
"Addr2": "{dispatch_details.address_line2}",
|
||||||
|
"Loc": "{dispatch_details.location}",
|
||||||
|
"Pin": "{dispatch_details.pincode}",
|
||||||
|
"Stcd": "{dispatch_details.state_code}"
|
||||||
|
}},
|
||||||
|
"ShipDtls": {{
|
||||||
|
"Gstin": "{shipping_details.gstin}",
|
||||||
|
"LglNm": "{shipping_details.legal_name}",
|
||||||
|
"TrdNm": "{shipping_details.trader_name}",
|
||||||
|
"Addr1": "{shipping_details.address_line1}",
|
||||||
|
"Addr2": "{shipping_details.address_line2}",
|
||||||
|
"Loc": "{shipping_details.location}",
|
||||||
|
"Pin": "{shipping_details.pincode}",
|
||||||
|
"Stcd": "{shipping_details.state_code}"
|
||||||
|
}},
|
||||||
|
"ItemList": [
|
||||||
|
{item_list}
|
||||||
|
],
|
||||||
|
"ValDtls": {{
|
||||||
|
"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}",
|
||||||
|
"CesVal": "{invoice_value_details.total_cess_amt}",
|
||||||
|
"Discount": "{invoice_value_details.invoice_discount_amt}",
|
||||||
|
"RndOffAmt": "{invoice_value_details.round_off}",
|
||||||
|
"OthChrg": "{invoice_value_details.total_other_charges}",
|
||||||
|
"TotInvVal": "{invoice_value_details.base_grand_total}",
|
||||||
|
"TotInvValFc": "{invoice_value_details.grand_total}"
|
||||||
|
}},
|
||||||
|
"PayDtls": {{
|
||||||
|
"Nm": "{payment_details.payee_name}",
|
||||||
|
"AccDet": "{payment_details.account_no}",
|
||||||
|
"Mode": "{payment_details.mode_of_payment}",
|
||||||
|
"FinInsBr": "{payment_details.ifsc_code}",
|
||||||
|
"PayTerm": "{payment_details.terms}",
|
||||||
|
"PaidAmt": "{payment_details.paid_amount}",
|
||||||
|
"PaymtDue": "{payment_details.outstanding_amount}"
|
||||||
|
}},
|
||||||
|
"RefDtls": {{
|
||||||
|
"DocPerdDtls": {{
|
||||||
|
"InvStDt": "{period_details.start_date}",
|
||||||
|
"InvEndDt": "{period_details.end_date}"
|
||||||
|
}},
|
||||||
|
"PrecDocDtls": [{{
|
||||||
|
"InvNo": "{prev_doc_details.invoice_name}",
|
||||||
|
"InvDt": "{prev_doc_details.invoice_date}"
|
||||||
|
}}]
|
||||||
|
}},
|
||||||
|
"ExpDtls": {{
|
||||||
|
"ShipBNo": "{export_details.bill_no}",
|
||||||
|
"ShipBDt": "{export_details.bill_date}",
|
||||||
|
"Port": "{export_details.port}",
|
||||||
|
"ForCur": "{export_details.foreign_curr_code}",
|
||||||
|
"CntCode": "{export_details.country_code}",
|
||||||
|
"ExpDuty": "{export_details.export_duty}"
|
||||||
|
}},
|
||||||
|
"EwbDtls": {{
|
||||||
|
"TransId": "{eway_bill_details.gstin}",
|
||||||
|
"TransName": "{eway_bill_details.name}",
|
||||||
|
"TransMode": "{eway_bill_details.mode_of_transport}",
|
||||||
|
"Distance": "{eway_bill_details.distance}",
|
||||||
|
"TransDocNo": "{eway_bill_details.document_name}",
|
||||||
|
"TransDocDt": "{eway_bill_details.document_date}",
|
||||||
|
"VehNo": "{eway_bill_details.vehicle_no}",
|
||||||
|
"VehType": "{eway_bill_details.vehicle_type}"
|
||||||
|
}}
|
||||||
|
}}
|
956
erpnext/regional/india/e_invoice/einv_validation.json
Normal file
956
erpnext/regional/india/e_invoice/einv_validation.json
Normal file
@ -0,0 +1,956 @@
|
|||||||
|
{
|
||||||
|
"Version": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 6,
|
||||||
|
"description": "Version of the schema"
|
||||||
|
},
|
||||||
|
"Irn": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 64,
|
||||||
|
"maxLength": 64,
|
||||||
|
"description": "Invoice Reference Number"
|
||||||
|
},
|
||||||
|
"TranDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"TaxSch": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 10,
|
||||||
|
"enum": ["GST"],
|
||||||
|
"description": "GST- Goods and Services Tax Scheme"
|
||||||
|
},
|
||||||
|
"SupTyp": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 10,
|
||||||
|
"enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"],
|
||||||
|
"description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export"
|
||||||
|
},
|
||||||
|
"RegRev": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 1,
|
||||||
|
"enum": ["Y", "N"],
|
||||||
|
"description": "Y- whether the tax liability is payable under reverse charge"
|
||||||
|
},
|
||||||
|
"EcmGstin": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 15,
|
||||||
|
"maxLength": 15,
|
||||||
|
"pattern": "([0-9]{2}[0-9A-Z]{13})",
|
||||||
|
"description": "E-Commerce GSTIN",
|
||||||
|
"validationMsg": "E-Commerce GSTIN is invalid"
|
||||||
|
},
|
||||||
|
"IgstOnIntra": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 1,
|
||||||
|
"enum": ["Y", "N"],
|
||||||
|
"description": "Y- indicates the supply is intra state but chargeable to IGST"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["TaxSch", "SupTyp"]
|
||||||
|
},
|
||||||
|
"DocDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Typ": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 3,
|
||||||
|
"enum": ["INV", "CRN", "DBN"],
|
||||||
|
"description": "Document Type"
|
||||||
|
},
|
||||||
|
"No": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 16,
|
||||||
|
"pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$",
|
||||||
|
"description": "Document Number",
|
||||||
|
"validationMsg": "Document Number should not be starting with 0, / and -"
|
||||||
|
},
|
||||||
|
"Dt": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10,
|
||||||
|
"maxLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Document Date"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["Typ", "No", "Dt"]
|
||||||
|
},
|
||||||
|
"SellerDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Gstin": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 15,
|
||||||
|
"maxLength": 15,
|
||||||
|
"pattern": "([0-9]{2}[0-9A-Z]{13})",
|
||||||
|
"description": "Supplier GSTIN",
|
||||||
|
"validationMsg": "Company GSTIN is invalid"
|
||||||
|
},
|
||||||
|
"LglNm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Legal Name"
|
||||||
|
},
|
||||||
|
"TrdNm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Tradename"
|
||||||
|
},
|
||||||
|
"Addr1": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Address Line 1"
|
||||||
|
},
|
||||||
|
"Addr2": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Address Line 2"
|
||||||
|
},
|
||||||
|
"Loc": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 50,
|
||||||
|
"description": "Location"
|
||||||
|
},
|
||||||
|
"Pin": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 100000,
|
||||||
|
"maximum": 999999,
|
||||||
|
"description": "Pincode"
|
||||||
|
},
|
||||||
|
"Stcd": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 2,
|
||||||
|
"description": "Supplier State Code"
|
||||||
|
},
|
||||||
|
"Ph": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 6,
|
||||||
|
"maxLength": 12,
|
||||||
|
"description": "Phone"
|
||||||
|
},
|
||||||
|
"Em": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 6,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Email-Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"]
|
||||||
|
},
|
||||||
|
"BuyerDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Gstin": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 15,
|
||||||
|
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
|
||||||
|
"description": "Buyer GSTIN",
|
||||||
|
"validationMsg": "Customer GSTIN is invalid"
|
||||||
|
},
|
||||||
|
"LglNm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Legal Name"
|
||||||
|
},
|
||||||
|
"TrdNm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Trade Name"
|
||||||
|
},
|
||||||
|
"Pos": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 2,
|
||||||
|
"description": "Place of Supply State code"
|
||||||
|
},
|
||||||
|
"Addr1": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Address Line 1"
|
||||||
|
},
|
||||||
|
"Addr2": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Address Line 2"
|
||||||
|
},
|
||||||
|
"Loc": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Location"
|
||||||
|
},
|
||||||
|
"Pin": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 100000,
|
||||||
|
"maximum": 999999,
|
||||||
|
"description": "Pincode"
|
||||||
|
},
|
||||||
|
"Stcd": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 2,
|
||||||
|
"description": "Buyer State Code"
|
||||||
|
},
|
||||||
|
"Ph": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 6,
|
||||||
|
"maxLength": 12,
|
||||||
|
"description": "Phone"
|
||||||
|
},
|
||||||
|
"Em": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 6,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Email-Id"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
|
||||||
|
},
|
||||||
|
"DispDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Nm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Dispatch Address Name"
|
||||||
|
},
|
||||||
|
"Addr1": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Address Line 1"
|
||||||
|
},
|
||||||
|
"Addr2": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Address Line 2"
|
||||||
|
},
|
||||||
|
"Loc": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Location"
|
||||||
|
},
|
||||||
|
"Pin": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 100000,
|
||||||
|
"maximum": 999999,
|
||||||
|
"description": "Pincode"
|
||||||
|
},
|
||||||
|
"Stcd": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 2,
|
||||||
|
"description": "State Code"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"]
|
||||||
|
},
|
||||||
|
"ShipDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Gstin": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 15,
|
||||||
|
"minLength": 3,
|
||||||
|
"pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
|
||||||
|
"description": "Shipping Address GSTIN",
|
||||||
|
"validationMsg": "Shipping Address GSTIN is invalid"
|
||||||
|
},
|
||||||
|
"LglNm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Legal Name"
|
||||||
|
},
|
||||||
|
"TrdNm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Trade Name"
|
||||||
|
},
|
||||||
|
"Addr1": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Address Line 1"
|
||||||
|
},
|
||||||
|
"Addr2": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Address Line 2"
|
||||||
|
},
|
||||||
|
"Loc": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Location"
|
||||||
|
},
|
||||||
|
"Pin": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 100000,
|
||||||
|
"maximum": 999999,
|
||||||
|
"description": "Pincode"
|
||||||
|
},
|
||||||
|
"Stcd": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 2,
|
||||||
|
"description": "State Code"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
|
||||||
|
},
|
||||||
|
"ItemList": {
|
||||||
|
"type": "Array",
|
||||||
|
"properties": {
|
||||||
|
"SlNo": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 6,
|
||||||
|
"description": "Serial No. of Item"
|
||||||
|
},
|
||||||
|
"PrdDesc": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 300,
|
||||||
|
"description": "Item Name"
|
||||||
|
},
|
||||||
|
"IsServc": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 1,
|
||||||
|
"enum": ["Y", "N"],
|
||||||
|
"description": "Is Service Item"
|
||||||
|
},
|
||||||
|
"HsnCd": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 4,
|
||||||
|
"maxLength": 8,
|
||||||
|
"description": "HSN Code"
|
||||||
|
},
|
||||||
|
"Barcde": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 30,
|
||||||
|
"description": "Barcode"
|
||||||
|
},
|
||||||
|
"Qty": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9999999999.999,
|
||||||
|
"description": "Quantity"
|
||||||
|
},
|
||||||
|
"FreeQty": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9999999999.999,
|
||||||
|
"description": "Free Quantity"
|
||||||
|
},
|
||||||
|
"Unit": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 8,
|
||||||
|
"description": "UOM"
|
||||||
|
},
|
||||||
|
"UnitPrice": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.999,
|
||||||
|
"description": "Rate"
|
||||||
|
},
|
||||||
|
"TotAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Gross Amount"
|
||||||
|
},
|
||||||
|
"Discount": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Discount"
|
||||||
|
},
|
||||||
|
"PreTaxVal": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Pre tax value"
|
||||||
|
},
|
||||||
|
"AssAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Taxable Value"
|
||||||
|
},
|
||||||
|
"GstRt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999.999,
|
||||||
|
"description": "GST Rate"
|
||||||
|
},
|
||||||
|
"IgstAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "IGST Amount"
|
||||||
|
},
|
||||||
|
"CgstAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "CGST Amount"
|
||||||
|
},
|
||||||
|
"SgstAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "SGST Amount"
|
||||||
|
},
|
||||||
|
"CesRt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999.999,
|
||||||
|
"description": "Cess Rate"
|
||||||
|
},
|
||||||
|
"CesAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Cess Amount (Advalorem)"
|
||||||
|
},
|
||||||
|
"CesNonAdvlAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Cess Amount (Non-Advalorem)"
|
||||||
|
},
|
||||||
|
"StateCesRt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999.999,
|
||||||
|
"description": "State CESS Rate"
|
||||||
|
},
|
||||||
|
"StateCesAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "State CESS Amount"
|
||||||
|
},
|
||||||
|
"StateCesNonAdvlAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "State CESS Amount (Non Advalorem)"
|
||||||
|
},
|
||||||
|
"OthChrg": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Other Charges"
|
||||||
|
},
|
||||||
|
"TotItemVal": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Total Item Value"
|
||||||
|
},
|
||||||
|
"OrdLineRef": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 50,
|
||||||
|
"description": "Order line reference"
|
||||||
|
},
|
||||||
|
"OrgCntry": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 2,
|
||||||
|
"maxLength": 2,
|
||||||
|
"description": "Origin Country"
|
||||||
|
},
|
||||||
|
"PrdSlNo": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 20,
|
||||||
|
"description": "Serial number"
|
||||||
|
},
|
||||||
|
"BchDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Nm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 20,
|
||||||
|
"description": "Batch number"
|
||||||
|
},
|
||||||
|
"ExpDt": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 10,
|
||||||
|
"minLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Batch Expiry Date"
|
||||||
|
},
|
||||||
|
"WrDt": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 10,
|
||||||
|
"minLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Warranty Date"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["Nm"]
|
||||||
|
},
|
||||||
|
"AttribDtls": {
|
||||||
|
"type": "Array",
|
||||||
|
"Attribute": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Nm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Attribute name of the item"
|
||||||
|
},
|
||||||
|
"Val": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Attribute value of the item"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"SlNo",
|
||||||
|
"IsServc",
|
||||||
|
"HsnCd",
|
||||||
|
"UnitPrice",
|
||||||
|
"TotAmt",
|
||||||
|
"AssAmt",
|
||||||
|
"GstRt",
|
||||||
|
"TotItemVal"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ValDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"AssVal": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Total Assessable value of all items"
|
||||||
|
},
|
||||||
|
"CgstVal": {
|
||||||
|
"type": "number",
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"minimum": 0,
|
||||||
|
"description": "Total CGST value of all items"
|
||||||
|
},
|
||||||
|
"SgstVal": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Total SGST value of all items"
|
||||||
|
},
|
||||||
|
"IgstVal": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Total IGST value of all items"
|
||||||
|
},
|
||||||
|
"CesVal": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Total CESS value of all items"
|
||||||
|
},
|
||||||
|
"StCesVal": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Total State CESS value of all items"
|
||||||
|
},
|
||||||
|
"Discount": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Invoice Discount"
|
||||||
|
},
|
||||||
|
"OthChrg": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Other Charges"
|
||||||
|
},
|
||||||
|
"RndOffAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": -99.99,
|
||||||
|
"maximum": 99.99,
|
||||||
|
"description": "Rounded off Amount"
|
||||||
|
},
|
||||||
|
"TotInvVal": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Final Invoice Value "
|
||||||
|
},
|
||||||
|
"TotInvValFc": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Final Invoice value in Foreign Currency"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["AssVal", "TotInvVal"]
|
||||||
|
},
|
||||||
|
"PayDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"Nm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Payee Name"
|
||||||
|
},
|
||||||
|
"AccDet": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 18,
|
||||||
|
"description": "Bank Account Number of Payee"
|
||||||
|
},
|
||||||
|
"Mode": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 18,
|
||||||
|
"description": "Mode of Payment"
|
||||||
|
},
|
||||||
|
"FinInsBr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 11,
|
||||||
|
"description": "Branch or IFSC code"
|
||||||
|
},
|
||||||
|
"PayTerm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Terms of Payment"
|
||||||
|
},
|
||||||
|
"PayInstr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Payment Instruction"
|
||||||
|
},
|
||||||
|
"CrTrn": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Credit Transfer"
|
||||||
|
},
|
||||||
|
"DirDr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Direct Debit"
|
||||||
|
},
|
||||||
|
"CrDay": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 9999,
|
||||||
|
"description": "Credit Days"
|
||||||
|
},
|
||||||
|
"PaidAmt": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Advance Amount"
|
||||||
|
},
|
||||||
|
"PaymtDue": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 99999999999999.99,
|
||||||
|
"description": "Outstanding Amount"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"RefDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"InvRm": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 100,
|
||||||
|
"minLength": 3,
|
||||||
|
"pattern": "^[0-9A-Za-z/-]{3,100}$",
|
||||||
|
"description": "Remarks/Note"
|
||||||
|
},
|
||||||
|
"DocPerdDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"InvStDt": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 10,
|
||||||
|
"minLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Invoice Period Start Date"
|
||||||
|
},
|
||||||
|
"InvEndDt": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 10,
|
||||||
|
"minLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Invoice Period End Date"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["InvStDt ", "InvEndDt "]
|
||||||
|
},
|
||||||
|
"PrecDocDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"InvNo": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 16,
|
||||||
|
"pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$",
|
||||||
|
"description": "Reference of Original Invoice"
|
||||||
|
},
|
||||||
|
"InvDt": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 10,
|
||||||
|
"minLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Date of Orginal Invoice"
|
||||||
|
},
|
||||||
|
"OthRefNo": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 20,
|
||||||
|
"description": "Other Reference"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["InvNo", "InvDt"],
|
||||||
|
"ContrDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"RecAdvRefr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 20,
|
||||||
|
"pattern": "^([0-9A-Za-z/-]){1,20}$",
|
||||||
|
"description": "Receipt Advice No."
|
||||||
|
},
|
||||||
|
"RecAdvDt": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10,
|
||||||
|
"maxLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Date of receipt advice"
|
||||||
|
},
|
||||||
|
"TendRefr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 20,
|
||||||
|
"pattern": "^([0-9A-Za-z/-]){1,20}$",
|
||||||
|
"description": "Lot/Batch Reference No."
|
||||||
|
},
|
||||||
|
"ContrRefr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 20,
|
||||||
|
"pattern": "^([0-9A-Za-z/-]){1,20}$",
|
||||||
|
"description": "Contract Reference Number"
|
||||||
|
},
|
||||||
|
"ExtRefr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 20,
|
||||||
|
"pattern": "^([0-9A-Za-z/-]){1,20}$",
|
||||||
|
"description": "Any other reference"
|
||||||
|
},
|
||||||
|
"ProjRefr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 20,
|
||||||
|
"pattern": "^([0-9A-Za-z/-]){1,20}$",
|
||||||
|
"description": "Project Reference Number"
|
||||||
|
},
|
||||||
|
"PORefr": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 16,
|
||||||
|
"pattern": "^([0-9A-Za-z/-]){1,16}$",
|
||||||
|
"description": "PO Reference Number"
|
||||||
|
},
|
||||||
|
"PORefDt": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10,
|
||||||
|
"maxLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "PO Reference date"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AddlDocDtls": {
|
||||||
|
"type": "Array",
|
||||||
|
"properties": {
|
||||||
|
"Url": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Supporting document URL"
|
||||||
|
},
|
||||||
|
"Docs": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 1000,
|
||||||
|
"description": "Supporting document in Base64 Format"
|
||||||
|
},
|
||||||
|
"Info": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 1000,
|
||||||
|
"description": "Any additional information"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
"ExpDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ShipBNo": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 20,
|
||||||
|
"description": "Shipping Bill No."
|
||||||
|
},
|
||||||
|
"ShipBDt": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10,
|
||||||
|
"maxLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Shipping Bill Date"
|
||||||
|
},
|
||||||
|
"Port": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 2,
|
||||||
|
"maxLength": 10,
|
||||||
|
"pattern": "^[0-9A-Za-z]{2,10}$",
|
||||||
|
"description": "Port Code. Refer the master"
|
||||||
|
},
|
||||||
|
"RefClm": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 1,
|
||||||
|
"description": "Claiming Refund. Y/N"
|
||||||
|
},
|
||||||
|
"ForCur": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 16,
|
||||||
|
"description": "Additional Currency Code. Refer the master"
|
||||||
|
},
|
||||||
|
"CntCode": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 2,
|
||||||
|
"maxLength": 2,
|
||||||
|
"description": "Country Code. Refer the master"
|
||||||
|
},
|
||||||
|
"ExpDuty": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 0,
|
||||||
|
"maximum": 999999999999.99,
|
||||||
|
"description": "Export Duty"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"EwbDtls": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"TransId": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 15,
|
||||||
|
"maxLength": 15,
|
||||||
|
"description": "Transporter GSTIN"
|
||||||
|
},
|
||||||
|
"TransName": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 3,
|
||||||
|
"maxLength": 100,
|
||||||
|
"description": "Transporter Name"
|
||||||
|
},
|
||||||
|
"TransMode": {
|
||||||
|
"type": "string",
|
||||||
|
"maxLength": 1,
|
||||||
|
"minLength": 1,
|
||||||
|
"enum": ["1", "2", "3", "4"],
|
||||||
|
"description": "Mode of Transport"
|
||||||
|
},
|
||||||
|
"Distance": {
|
||||||
|
"type": "number",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 9999,
|
||||||
|
"description": "Distance"
|
||||||
|
},
|
||||||
|
"TransDocNo": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 15,
|
||||||
|
"pattern": "^([0-9A-Z/-]){1,15}$",
|
||||||
|
"description": "Tranport Document Number"
|
||||||
|
},
|
||||||
|
"TransDocDt": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 10,
|
||||||
|
"maxLength": 10,
|
||||||
|
"pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
|
||||||
|
"description": "Transport Document Date"
|
||||||
|
},
|
||||||
|
"VehNo": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 4,
|
||||||
|
"maxLength": 20,
|
||||||
|
"description": "Vehicle Number"
|
||||||
|
},
|
||||||
|
"VehType": {
|
||||||
|
"type": "string",
|
||||||
|
"minLength": 1,
|
||||||
|
"maxLength": 1,
|
||||||
|
"enum": ["O", "R"],
|
||||||
|
"description": "Vehicle Type"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": ["Distance"]
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"Version",
|
||||||
|
"TranDtls",
|
||||||
|
"DocDtls",
|
||||||
|
"SellerDtls",
|
||||||
|
"BuyerDtls",
|
||||||
|
"ItemList",
|
||||||
|
"ValDtls"
|
||||||
|
]
|
||||||
|
}
|
305
erpnext/regional/india/e_invoice/einvoice.js
Normal file
305
erpnext/regional/india/e_invoice/einvoice.js
Normal file
@ -0,0 +1,305 @@
|
|||||||
|
erpnext.setup_einvoice_actions = (doctype) => {
|
||||||
|
frappe.ui.form.on(doctype, {
|
||||||
|
refresh(frm) {
|
||||||
|
const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
|
||||||
|
const supply_type = frm.doc.gst_category;
|
||||||
|
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
|
||||||
|
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
|
||||||
|
|
||||||
|
if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
|
||||||
|
|
||||||
|
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
|
||||||
|
|
||||||
|
const add_custom_button = (label, action) => {
|
||||||
|
if (!frm.custom_buttons[label]) {
|
||||||
|
frm.add_custom_button(label, action, __('E Invoicing'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!irn && !__unsaved) {
|
||||||
|
const action = () => {
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
|
||||||
|
args: { doctype, docname: name },
|
||||||
|
freeze: true,
|
||||||
|
callback: (res) => {
|
||||||
|
const einvoice = res.message;
|
||||||
|
show_einvoice_preview(frm, einvoice);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
add_custom_button(__("Generate IRN"), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (irn && !irn_cancelled && !ewaybill) {
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
"label": "Reason",
|
||||||
|
"fieldname": "reason",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"reqd": 1,
|
||||||
|
"default": "1-Duplicate",
|
||||||
|
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Remark",
|
||||||
|
"fieldname": "remark",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const action = () => {
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __("Cancel IRN"),
|
||||||
|
fields: fields,
|
||||||
|
primary_action: function() {
|
||||||
|
const data = d.get_values();
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.cancel_irn',
|
||||||
|
args: {
|
||||||
|
doctype,
|
||||||
|
docname: name,
|
||||||
|
irn: irn,
|
||||||
|
reason: data.reason.split('-')[0],
|
||||||
|
remark: data.remark
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: () => frm.reload_doc() || d.hide(),
|
||||||
|
error: () => d.hide()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
primary_action_label: __('Submit')
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
};
|
||||||
|
add_custom_button(__("Cancel IRN"), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (irn && !irn_cancelled && !ewaybill) {
|
||||||
|
const action = () => {
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __('Generate E-Way Bill'),
|
||||||
|
wide: 1,
|
||||||
|
fields: get_ewaybill_fields(frm),
|
||||||
|
primary_action: function() {
|
||||||
|
const data = d.get_values();
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill',
|
||||||
|
args: {
|
||||||
|
doctype,
|
||||||
|
docname: name,
|
||||||
|
irn,
|
||||||
|
...data
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: () => frm.reload_doc() || d.hide(),
|
||||||
|
error: () => d.hide()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
primary_action_label: __('Submit')
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
};
|
||||||
|
|
||||||
|
add_custom_button(__("Generate E-Way Bill"), action);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||||
|
const fields = [
|
||||||
|
{
|
||||||
|
"label": "Reason",
|
||||||
|
"fieldname": "reason",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"reqd": 1,
|
||||||
|
"default": "1-Duplicate",
|
||||||
|
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Remark",
|
||||||
|
"fieldname": "remark",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
];
|
||||||
|
const action = () => {
|
||||||
|
const d = new frappe.ui.Dialog({
|
||||||
|
title: __('Cancel E-Way Bill'),
|
||||||
|
fields: fields,
|
||||||
|
primary_action: function() {
|
||||||
|
const data = d.get_values();
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||||
|
args: {
|
||||||
|
doctype,
|
||||||
|
docname: name,
|
||||||
|
eway_bill: ewaybill,
|
||||||
|
reason: data.reason.split('-')[0],
|
||||||
|
remark: data.remark
|
||||||
|
},
|
||||||
|
freeze: true,
|
||||||
|
callback: () => frm.reload_doc() || d.hide(),
|
||||||
|
error: () => d.hide()
|
||||||
|
});
|
||||||
|
},
|
||||||
|
primary_action_label: __('Submit')
|
||||||
|
});
|
||||||
|
d.show();
|
||||||
|
};
|
||||||
|
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const get_ewaybill_fields = (frm) => {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'fieldname': 'transporter',
|
||||||
|
'label': 'Transporter',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Supplier',
|
||||||
|
'default': frm.doc.transporter
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'gst_transporter_id',
|
||||||
|
'label': 'GST Transporter ID',
|
||||||
|
'fieldtype': 'Data',
|
||||||
|
'fetch_from': 'transporter.gst_transporter_id',
|
||||||
|
'default': frm.doc.gst_transporter_id
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'driver',
|
||||||
|
'label': 'Driver',
|
||||||
|
'fieldtype': 'Link',
|
||||||
|
'options': 'Driver',
|
||||||
|
'default': frm.doc.driver
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'lr_no',
|
||||||
|
'label': 'Transport Receipt No',
|
||||||
|
'fieldtype': 'Data',
|
||||||
|
'default': frm.doc.lr_no
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'vehicle_no',
|
||||||
|
'label': 'Vehicle No',
|
||||||
|
'fieldtype': 'Data',
|
||||||
|
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
|
||||||
|
'default': frm.doc.vehicle_no
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'distance',
|
||||||
|
'label': 'Distance (in km)',
|
||||||
|
'fieldtype': 'Float',
|
||||||
|
'default': frm.doc.distance
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'transporter_col_break',
|
||||||
|
'fieldtype': 'Column Break',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'transporter_name',
|
||||||
|
'label': 'Transporter Name',
|
||||||
|
'fieldtype': 'Data',
|
||||||
|
'fetch_from': 'transporter.name',
|
||||||
|
'read_only': 1,
|
||||||
|
'default': frm.doc.transporter_name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'mode_of_transport',
|
||||||
|
'label': 'Mode of Transport',
|
||||||
|
'fieldtype': 'Select',
|
||||||
|
'options': `\nRoad\nAir\nRail\nShip`,
|
||||||
|
'default': frm.doc.mode_of_transport
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'driver_name',
|
||||||
|
'label': 'Driver Name',
|
||||||
|
'fieldtype': 'Data',
|
||||||
|
'fetch_from': 'driver.full_name',
|
||||||
|
'read_only': 1,
|
||||||
|
'default': frm.doc.driver_name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'lr_date',
|
||||||
|
'label': 'Transport Receipt Date',
|
||||||
|
'fieldtype': 'Date',
|
||||||
|
'default': frm.doc.lr_date
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'fieldname': 'gst_vehicle_type',
|
||||||
|
'label': 'GST Vehicle Type',
|
||||||
|
'fieldtype': 'Select',
|
||||||
|
'options': `Regular\nOver Dimensional Cargo (ODC)`,
|
||||||
|
'depends_on': 'eval:(doc.mode_of_transport === "Road")',
|
||||||
|
'default': frm.doc.gst_vehicle_type
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const request_irn_generation = (frm) => {
|
||||||
|
frappe.call({
|
||||||
|
method: 'erpnext.regional.india.e_invoice.utils.generate_irn',
|
||||||
|
args: { doctype: frm.doc.doctype, docname: frm.doc.name },
|
||||||
|
freeze: true,
|
||||||
|
callback: () => frm.reload_doc()
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const get_preview_dialog = (frm, action) => {
|
||||||
|
const dialog = new frappe.ui.Dialog({
|
||||||
|
title: __("Preview"),
|
||||||
|
wide: 1,
|
||||||
|
fields: [
|
||||||
|
{
|
||||||
|
"label": "Preview",
|
||||||
|
"fieldname": "preview_html",
|
||||||
|
"fieldtype": "HTML"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
primary_action: () => action(frm) || dialog.hide(),
|
||||||
|
primary_action_label: __('Generate IRN')
|
||||||
|
});
|
||||||
|
return dialog;
|
||||||
|
};
|
||||||
|
|
||||||
|
const show_einvoice_preview = (frm, einvoice) => {
|
||||||
|
const preview_dialog = get_preview_dialog(frm, request_irn_generation);
|
||||||
|
|
||||||
|
// initialize e-invoice fields
|
||||||
|
einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate();
|
||||||
|
frm.doc.signed_einvoice = JSON.stringify(einvoice);
|
||||||
|
|
||||||
|
// initialize preview wrapper
|
||||||
|
const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper;
|
||||||
|
$preview_wrapper.html(
|
||||||
|
`<div>
|
||||||
|
<div class="print-preview">
|
||||||
|
<div class="print-format"></div>
|
||||||
|
</div>
|
||||||
|
<div class="page-break-message text-muted text-center text-medium margin-top"></div>
|
||||||
|
</div>`
|
||||||
|
);
|
||||||
|
|
||||||
|
frappe.call({
|
||||||
|
method: "frappe.www.printview.get_html_and_style",
|
||||||
|
args: {
|
||||||
|
doc: frm.doc,
|
||||||
|
print_format: "GST E-Invoice",
|
||||||
|
no_letterhead: 1
|
||||||
|
},
|
||||||
|
callback: function (r) {
|
||||||
|
if (!r.exc) {
|
||||||
|
$preview_wrapper.find(".print-format").html(r.message.html);
|
||||||
|
const style = `
|
||||||
|
.print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; }
|
||||||
|
.print-preview { min-height: 0px; }
|
||||||
|
.modal-dialog { width: 720px; }`;
|
||||||
|
|
||||||
|
frappe.dom.set_style(style, "custom-print-style");
|
||||||
|
preview_dialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
772
erpnext/regional/india/e_invoice/utils.py
Normal file
772
erpnext/regional/india/e_invoice/utils.py
Normal file
@ -0,0 +1,772 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import jwt
|
||||||
|
import sys
|
||||||
|
import json
|
||||||
|
import base64
|
||||||
|
import frappe
|
||||||
|
import traceback
|
||||||
|
from frappe import _, bold
|
||||||
|
from pyqrcode import create as qrcreate
|
||||||
|
from frappe.integrations.utils import make_post_request, make_get_request
|
||||||
|
from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
|
||||||
|
from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date
|
||||||
|
|
||||||
|
def validate_einvoice_fields(doc):
|
||||||
|
einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
|
||||||
|
invalid_doctype = doc.doctype not in ['Sales Invoice']
|
||||||
|
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
|
||||||
|
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
||||||
|
|
||||||
|
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction: return
|
||||||
|
|
||||||
|
if doc.docstatus == 0 and doc._action == 'save':
|
||||||
|
if doc.irn:
|
||||||
|
frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed'))
|
||||||
|
if len(doc.name) > 16:
|
||||||
|
raise_document_name_too_long_error()
|
||||||
|
|
||||||
|
elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
|
||||||
|
frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
|
||||||
|
|
||||||
|
elif doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
|
||||||
|
frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed'))
|
||||||
|
|
||||||
|
def raise_document_name_too_long_error():
|
||||||
|
title = _('Document ID Too Long')
|
||||||
|
msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice, ')
|
||||||
|
msg += _('document id {} exceed 16 letters. ').format(bold(_('should not')))
|
||||||
|
msg += '<br><br>'
|
||||||
|
msg += _('You must {} your {} in order to have document id of {} length 16. ').format(
|
||||||
|
bold(_('modify')), bold(_('naming series')), bold(_('maximum'))
|
||||||
|
)
|
||||||
|
msg += _('Please account for ammended documents too. ')
|
||||||
|
frappe.throw(msg, title=title)
|
||||||
|
|
||||||
|
def read_json(name):
|
||||||
|
file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name))
|
||||||
|
with open(file_path, 'r') as f:
|
||||||
|
return cstr(f.read())
|
||||||
|
|
||||||
|
def get_transaction_details(invoice):
|
||||||
|
supply_type = ''
|
||||||
|
if invoice.gst_category == 'Registered Regular': supply_type = 'B2B'
|
||||||
|
elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP'
|
||||||
|
elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP'
|
||||||
|
elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP'
|
||||||
|
|
||||||
|
if not supply_type:
|
||||||
|
rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export')
|
||||||
|
frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export),
|
||||||
|
title=_('Invalid Supply Type'))
|
||||||
|
|
||||||
|
return frappe._dict(dict(
|
||||||
|
tax_scheme='GST',
|
||||||
|
supply_type=supply_type,
|
||||||
|
reverse_charge=invoice.reverse_charge
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_doc_details(invoice):
|
||||||
|
invoice_type = 'CRN' if invoice.is_return else 'INV'
|
||||||
|
|
||||||
|
invoice_name = invoice.name
|
||||||
|
invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy')
|
||||||
|
|
||||||
|
return frappe._dict(dict(
|
||||||
|
invoice_type=invoice_type,
|
||||||
|
invoice_name=invoice_name,
|
||||||
|
invoice_date=invoice_date
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_party_details(address_name):
|
||||||
|
address = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
|
||||||
|
gstin = address.get('gstin')
|
||||||
|
|
||||||
|
gstin_details = get_gstin_details(gstin)
|
||||||
|
legal_name = gstin_details.get('LegalName') or gstin_details.get('TradeName')
|
||||||
|
location = gstin_details.get('AddrLoc') or address.get('city')
|
||||||
|
state_code = gstin_details.get('StateCode')
|
||||||
|
pincode = gstin_details.get('AddrPncd')
|
||||||
|
address_line1 = '{} {}'.format(gstin_details.get('AddrBno'), gstin_details.get('AddrFlno'))
|
||||||
|
address_line2 = '{} {}'.format(gstin_details.get('AddrBnm'), gstin_details.get('AddrSt'))
|
||||||
|
email_id = address.get('email_id')
|
||||||
|
phone = address.get('phone')
|
||||||
|
# get last 10 digit
|
||||||
|
phone = phone.replace(" ", "")[-10:] if phone else ''
|
||||||
|
|
||||||
|
if state_code == 97:
|
||||||
|
# according to einvoice standard
|
||||||
|
pincode = 999999
|
||||||
|
|
||||||
|
return frappe._dict(dict(
|
||||||
|
gstin=gstin, legal_name=legal_name, location=location,
|
||||||
|
pincode=pincode, state_code=state_code, address_line1=address_line1,
|
||||||
|
address_line2=address_line2, email=email_id, phone=phone
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_gstin_details(gstin):
|
||||||
|
if not hasattr(frappe.local, 'gstin_cache'):
|
||||||
|
frappe.local.gstin_cache = {}
|
||||||
|
|
||||||
|
key = gstin
|
||||||
|
details = frappe.local.gstin_cache.get(key)
|
||||||
|
if details:
|
||||||
|
return details
|
||||||
|
|
||||||
|
details = frappe.cache().hget('gstin_cache', key)
|
||||||
|
if details:
|
||||||
|
frappe.local.gstin_cache[key] = details
|
||||||
|
return details
|
||||||
|
|
||||||
|
if not details:
|
||||||
|
return GSPConnector.get_gstin_details(gstin)
|
||||||
|
|
||||||
|
def get_overseas_address_details(address_name):
|
||||||
|
address_title, address_line1, address_line2, city, phone, email_id = frappe.db.get_value(
|
||||||
|
'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city', 'phone', 'email_id']
|
||||||
|
)
|
||||||
|
|
||||||
|
return frappe._dict(dict(
|
||||||
|
gstin='URP', legal_name=address_title, address_line1=address_line1,
|
||||||
|
address_line2=address_line2, email=email_id, phone=phone,
|
||||||
|
pincode=999999, state_code=96, place_of_supply=96, location=city
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_item_list(invoice):
|
||||||
|
item_list = []
|
||||||
|
|
||||||
|
for d in invoice.items:
|
||||||
|
einvoice_item_schema = read_json('einv_item_template')
|
||||||
|
item = frappe._dict({})
|
||||||
|
item.update(d.as_dict())
|
||||||
|
|
||||||
|
item.sr_no = d.idx
|
||||||
|
item.discount_amount = abs(item.discount_amount * item.qty)
|
||||||
|
item.description = d.item_name
|
||||||
|
item.qty = abs(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.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
|
||||||
|
item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
|
||||||
|
|
||||||
|
item = update_item_taxes(invoice, item)
|
||||||
|
|
||||||
|
item.total_value = abs(
|
||||||
|
item.taxable_value + item.igst_amount + item.sgst_amount +
|
||||||
|
item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges
|
||||||
|
)
|
||||||
|
einv_item = einvoice_item_schema.format(item=item)
|
||||||
|
item_list.append(einv_item)
|
||||||
|
|
||||||
|
return ', '.join(item_list)
|
||||||
|
|
||||||
|
def update_item_taxes(invoice, item):
|
||||||
|
gst_accounts = get_gst_accounts(invoice.company)
|
||||||
|
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
|
||||||
|
|
||||||
|
for attr in [
|
||||||
|
'tax_rate', 'cess_rate', 'cess_nadv_amount',
|
||||||
|
'cgst_amount', 'sgst_amount', 'igst_amount',
|
||||||
|
'cess_amount', 'cess_nadv_amount', 'other_charges'
|
||||||
|
]:
|
||||||
|
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_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_amount_after_discount)
|
||||||
|
else:
|
||||||
|
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[f'{tax_type}_account']:
|
||||||
|
item.tax_rate += item_tax_rate
|
||||||
|
item[f'{tax_type}_amount'] += abs(item_tax_amount)
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
def get_invoice_value_details(invoice):
|
||||||
|
invoice_value_details = frappe._dict(dict())
|
||||||
|
invoice_value_details.base_total = abs(invoice.base_total)
|
||||||
|
invoice_value_details.invoice_discount_amt = invoice.discount_amount
|
||||||
|
invoice_value_details.round_off = invoice.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)
|
||||||
|
|
||||||
|
invoice_value_details = update_invoice_taxes(invoice, invoice_value_details)
|
||||||
|
|
||||||
|
return invoice_value_details
|
||||||
|
|
||||||
|
def update_invoice_taxes(invoice, invoice_value_details):
|
||||||
|
gst_accounts = get_gst_accounts(invoice.company)
|
||||||
|
gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
|
||||||
|
|
||||||
|
invoice_value_details.total_cgst_amt = 0
|
||||||
|
invoice_value_details.total_sgst_amt = 0
|
||||||
|
invoice_value_details.total_igst_amt = 0
|
||||||
|
invoice_value_details.total_cess_amt = 0
|
||||||
|
invoice_value_details.total_other_charges = 0
|
||||||
|
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)
|
||||||
|
|
||||||
|
for tax_type in ['igst', 'cgst', 'sgst']:
|
||||||
|
if t.account_head in gst_accounts[f'{tax_type}_account']:
|
||||||
|
invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount)
|
||||||
|
else:
|
||||||
|
invoice_value_details.total_other_charges += abs(t.base_tax_amount)
|
||||||
|
|
||||||
|
return invoice_value_details
|
||||||
|
|
||||||
|
def get_payment_details(invoice):
|
||||||
|
payee_name = invoice.company
|
||||||
|
mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
|
||||||
|
paid_amount = invoice.base_paid_amount
|
||||||
|
outstanding_amount = invoice.outstanding_amount
|
||||||
|
|
||||||
|
return frappe._dict(dict(
|
||||||
|
payee_name=payee_name, mode_of_payment=mode_of_payment,
|
||||||
|
paid_amount=paid_amount, outstanding_amount=outstanding_amount
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_return_doc_reference(invoice):
|
||||||
|
invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
|
||||||
|
return frappe._dict(dict(
|
||||||
|
invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
|
||||||
|
))
|
||||||
|
|
||||||
|
def get_eway_bill_details(invoice):
|
||||||
|
if invoice.is_return:
|
||||||
|
frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed'))
|
||||||
|
|
||||||
|
mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
|
||||||
|
vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
|
||||||
|
|
||||||
|
return frappe._dict(dict(
|
||||||
|
gstin=invoice.gst_transporter_id,
|
||||||
|
name=invoice.transporter_name,
|
||||||
|
mode_of_transport=mode_of_transport[invoice.mode_of_transport],
|
||||||
|
distance=invoice.distance or 0,
|
||||||
|
document_name=invoice.lr_no,
|
||||||
|
document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'),
|
||||||
|
vehicle_no=invoice.vehicle_no,
|
||||||
|
vehicle_type=vehicle_type[invoice.gst_vehicle_type]
|
||||||
|
))
|
||||||
|
|
||||||
|
def make_einvoice(invoice):
|
||||||
|
schema = read_json('einv_template')
|
||||||
|
|
||||||
|
transaction_details = get_transaction_details(invoice)
|
||||||
|
item_list = get_item_list(invoice)
|
||||||
|
doc_details = get_doc_details(invoice)
|
||||||
|
invoice_value_details = get_invoice_value_details(invoice)
|
||||||
|
seller_details = get_party_details(invoice.company_address)
|
||||||
|
|
||||||
|
if invoice.gst_category == 'Overseas':
|
||||||
|
buyer_details = get_overseas_address_details(invoice.customer_address)
|
||||||
|
else:
|
||||||
|
buyer_details = get_party_details(invoice.customer_address)
|
||||||
|
place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin
|
||||||
|
place_of_supply = place_of_supply[:2]
|
||||||
|
buyer_details.update(dict(place_of_supply=place_of_supply))
|
||||||
|
|
||||||
|
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
|
||||||
|
if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
|
||||||
|
shipping_details = get_party_details(invoice.shipping_address_name)
|
||||||
|
|
||||||
|
if invoice.is_pos and invoice.base_paid_amount:
|
||||||
|
payment_details = get_payment_details(invoice)
|
||||||
|
|
||||||
|
if invoice.is_return and invoice.return_against:
|
||||||
|
prev_doc_details = get_return_doc_reference(invoice)
|
||||||
|
|
||||||
|
if invoice.transporter:
|
||||||
|
eway_bill_details = get_eway_bill_details(invoice)
|
||||||
|
|
||||||
|
# not yet implemented
|
||||||
|
dispatch_details = period_details = export_details = frappe._dict({})
|
||||||
|
|
||||||
|
einvoice = schema.format(
|
||||||
|
transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details,
|
||||||
|
seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details,
|
||||||
|
item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details,
|
||||||
|
period_details=period_details, prev_doc_details=prev_doc_details,
|
||||||
|
export_details=export_details, eway_bill_details=eway_bill_details
|
||||||
|
)
|
||||||
|
einvoice = json.loads(einvoice)
|
||||||
|
|
||||||
|
validations = json.loads(read_json('einv_validation'))
|
||||||
|
errors = validate_einvoice(validations, einvoice)
|
||||||
|
if errors:
|
||||||
|
message = "\n".join([
|
||||||
|
"E Invoice: ", json.dumps(einvoice, indent=4),
|
||||||
|
"-" * 50,
|
||||||
|
"Errors: ", json.dumps(errors, indent=4)
|
||||||
|
])
|
||||||
|
frappe.log_error(title="E Invoice Validation Failed", message=message)
|
||||||
|
frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1)
|
||||||
|
|
||||||
|
return einvoice
|
||||||
|
|
||||||
|
def validate_einvoice(validations, einvoice, errors=[]):
|
||||||
|
for fieldname, field_validation in validations.items():
|
||||||
|
value = einvoice.get(fieldname, None)
|
||||||
|
if not value or value == "None":
|
||||||
|
# remove keys with empty values
|
||||||
|
einvoice.pop(fieldname, None)
|
||||||
|
continue
|
||||||
|
|
||||||
|
value_type = field_validation.get("type").lower()
|
||||||
|
if value_type in ['object', 'array']:
|
||||||
|
child_validations = field_validation.get('properties')
|
||||||
|
|
||||||
|
if isinstance(value, list):
|
||||||
|
for d in value:
|
||||||
|
validate_einvoice(child_validations, d, errors)
|
||||||
|
if not d:
|
||||||
|
# remove empty dicts
|
||||||
|
einvoice.pop(fieldname, None)
|
||||||
|
else:
|
||||||
|
validate_einvoice(child_validations, value, errors)
|
||||||
|
if not value:
|
||||||
|
# remove empty dicts
|
||||||
|
einvoice.pop(fieldname, None)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# convert to int or str
|
||||||
|
if value_type == 'string':
|
||||||
|
einvoice[fieldname] = str(value)
|
||||||
|
elif value_type == 'number':
|
||||||
|
is_integer = '.' not in str(field_validation.get('maximum'))
|
||||||
|
precision = 3 if '.999' in str(field_validation.get('maximum')) else 2
|
||||||
|
einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value)
|
||||||
|
value = einvoice[fieldname]
|
||||||
|
|
||||||
|
max_length = field_validation.get('maxLength')
|
||||||
|
minimum = flt(field_validation.get('minimum'))
|
||||||
|
maximum = flt(field_validation.get('maximum'))
|
||||||
|
pattern_str = field_validation.get('pattern')
|
||||||
|
pattern = re.compile(pattern_str or '')
|
||||||
|
|
||||||
|
label = field_validation.get('description') or fieldname
|
||||||
|
|
||||||
|
if value_type == 'string' and len(value) > max_length:
|
||||||
|
errors.append(_('{} should not exceed {} characters').format(label, max_length))
|
||||||
|
if value_type == 'number' and (value > maximum or value < minimum):
|
||||||
|
errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum))
|
||||||
|
if pattern_str and not pattern.match(value):
|
||||||
|
errors.append(field_validation.get('validationMsg'))
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
class RequestFailed(Exception): pass
|
||||||
|
|
||||||
|
class GSPConnector():
|
||||||
|
def __init__(self, doctype=None, docname=None):
|
||||||
|
self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
|
||||||
|
self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
|
||||||
|
self.credentials = self.get_credentials()
|
||||||
|
|
||||||
|
self.base_url = 'https://gsp.adaequare.com'
|
||||||
|
self.authenticate_url = self.base_url + '/gsp/authenticate?grant_type=token'
|
||||||
|
self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
|
||||||
|
self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
|
||||||
|
self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
|
||||||
|
self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
|
||||||
|
self.cancel_ewaybill_url = self.base_url + '/enriched/ei/api/ewayapi'
|
||||||
|
self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
|
||||||
|
|
||||||
|
def get_credentials(self):
|
||||||
|
if self.invoice:
|
||||||
|
gstin = self.get_seller_gstin()
|
||||||
|
credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
|
||||||
|
else:
|
||||||
|
credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
|
||||||
|
return credentials
|
||||||
|
|
||||||
|
def get_seller_gstin(self):
|
||||||
|
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
|
||||||
|
if not gstin:
|
||||||
|
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
|
||||||
|
return gstin
|
||||||
|
|
||||||
|
def get_auth_token(self):
|
||||||
|
if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0:
|
||||||
|
self.fetch_auth_token()
|
||||||
|
|
||||||
|
return self.e_invoice_settings.auth_token
|
||||||
|
|
||||||
|
def make_request(self, request_type, url, headers=None, data=None):
|
||||||
|
if request_type == 'post':
|
||||||
|
res = make_post_request(url, headers=headers, data=data)
|
||||||
|
else:
|
||||||
|
res = make_get_request(url, headers=headers, data=data)
|
||||||
|
|
||||||
|
self.log_request(url, headers, data, res)
|
||||||
|
return res
|
||||||
|
|
||||||
|
def log_request(self, url, headers, data, res):
|
||||||
|
headers.update({ 'password': self.credentials.password })
|
||||||
|
request_log = frappe.get_doc({
|
||||||
|
"doctype": "E Invoice Request Log",
|
||||||
|
"user": frappe.session.user,
|
||||||
|
"reference_invoice": self.invoice.name if self.invoice else None,
|
||||||
|
"url": url,
|
||||||
|
"headers": json.dumps(headers, indent=4) if headers else None,
|
||||||
|
"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)
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
def fetch_auth_token(self):
|
||||||
|
headers = {
|
||||||
|
'gspappid': frappe.conf.einvoice_client_id,
|
||||||
|
'gspappsecret': frappe.conf.einvoice_client_secret
|
||||||
|
}
|
||||||
|
res = {}
|
||||||
|
try:
|
||||||
|
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()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.log_error(res)
|
||||||
|
self.raise_error(True)
|
||||||
|
|
||||||
|
def get_headers(self):
|
||||||
|
return {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'user_name': self.credentials.username,
|
||||||
|
'password': self.credentials.get_password(),
|
||||||
|
'gstin': self.credentials.gstin,
|
||||||
|
'authorization': self.get_auth_token(),
|
||||||
|
'requestid': str(base64.b64encode(os.urandom(18))),
|
||||||
|
}
|
||||||
|
|
||||||
|
def fetch_gstin_details(self, gstin):
|
||||||
|
headers = self.get_headers()
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = '?gstin={gstin}'.format(gstin=gstin)
|
||||||
|
res = self.make_request('get', self.gstin_details_url + params, headers)
|
||||||
|
if res.get('success'):
|
||||||
|
return res.get('result')
|
||||||
|
else:
|
||||||
|
self.log_error(res)
|
||||||
|
raise RequestFailed
|
||||||
|
|
||||||
|
except RequestFailed:
|
||||||
|
self.raise_error()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.log_error()
|
||||||
|
self.raise_error(True)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_gstin_details(gstin):
|
||||||
|
'''fetch and cache GSTIN details'''
|
||||||
|
if not hasattr(frappe.local, 'gstin_cache'):
|
||||||
|
frappe.local.gstin_cache = {}
|
||||||
|
|
||||||
|
key = gstin
|
||||||
|
gsp_connector = GSPConnector()
|
||||||
|
details = gsp_connector.fetch_gstin_details(gstin)
|
||||||
|
|
||||||
|
frappe.local.gstin_cache[key] = details
|
||||||
|
frappe.cache().hset('gstin_cache', key, details)
|
||||||
|
return details
|
||||||
|
|
||||||
|
def generate_irn(self):
|
||||||
|
headers = self.get_headers()
|
||||||
|
einvoice = make_einvoice(self.invoice)
|
||||||
|
data = json.dumps(einvoice, indent=4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self.make_request('post', self.generate_irn_url, headers, data)
|
||||||
|
if res.get('success'):
|
||||||
|
self.set_einvoice_data(res.get('result'))
|
||||||
|
|
||||||
|
elif '2150' in res.get('message'):
|
||||||
|
# IRN already generated but not updated in invoice
|
||||||
|
# Extract the IRN from the response description and fetch irn details
|
||||||
|
irn = res.get('result')[0].get('Desc').get('Irn')
|
||||||
|
irn_details = self.get_irn_details(irn)
|
||||||
|
if irn_details:
|
||||||
|
self.set_einvoice_data(irn_details)
|
||||||
|
else:
|
||||||
|
raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \
|
||||||
|
Contact ERPNext support to resolve the issue.')
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RequestFailed
|
||||||
|
|
||||||
|
except RequestFailed:
|
||||||
|
errors = self.sanitize_error_message(res.get('message'))
|
||||||
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.log_error(data)
|
||||||
|
self.raise_error(True)
|
||||||
|
|
||||||
|
def get_irn_details(self, irn):
|
||||||
|
headers = self.get_headers()
|
||||||
|
|
||||||
|
try:
|
||||||
|
params = '?irn={irn}'.format(irn=irn)
|
||||||
|
res = self.make_request('get', self.irn_details_url + params, headers)
|
||||||
|
if res.get('success'):
|
||||||
|
return res.get('result')
|
||||||
|
else:
|
||||||
|
raise RequestFailed
|
||||||
|
|
||||||
|
except RequestFailed:
|
||||||
|
errors = self.sanitize_error_message(res.get('message'))
|
||||||
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.log_error()
|
||||||
|
self.raise_error(True)
|
||||||
|
|
||||||
|
def cancel_irn(self, irn, reason, remark):
|
||||||
|
headers = self.get_headers()
|
||||||
|
data = json.dumps({
|
||||||
|
'Irn': irn,
|
||||||
|
'Cnlrsn': reason,
|
||||||
|
'Cnlrem': remark
|
||||||
|
}, indent=4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self.make_request('post', self.cancel_irn_url, headers, data)
|
||||||
|
if res.get('success'):
|
||||||
|
self.invoice.irn_cancelled = 1
|
||||||
|
self.invoice.flags.updater_reference = {
|
||||||
|
'doctype': self.invoice.doctype,
|
||||||
|
'docname': self.invoice.name,
|
||||||
|
'label': _('IRN Cancelled - {}').format(remark)
|
||||||
|
}
|
||||||
|
self.update_invoice()
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RequestFailed
|
||||||
|
|
||||||
|
except RequestFailed:
|
||||||
|
errors = self.sanitize_error_message(res.get('message'))
|
||||||
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.log_error(data)
|
||||||
|
self.raise_error(True)
|
||||||
|
|
||||||
|
def generate_eway_bill(self, **kwargs):
|
||||||
|
args = frappe._dict(kwargs)
|
||||||
|
|
||||||
|
headers = self.get_headers()
|
||||||
|
eway_bill_details = get_eway_bill_details(args)
|
||||||
|
data = json.dumps({
|
||||||
|
'Irn': args.irn,
|
||||||
|
'Distance': cint(eway_bill_details.distance),
|
||||||
|
'TransMode': eway_bill_details.mode_of_transport,
|
||||||
|
'TransId': eway_bill_details.gstin,
|
||||||
|
'TransName': eway_bill_details.transporter,
|
||||||
|
'TrnDocDt': eway_bill_details.document_date,
|
||||||
|
'TrnDocNo': eway_bill_details.document_name,
|
||||||
|
'VehNo': eway_bill_details.vehicle_no,
|
||||||
|
'VehType': eway_bill_details.vehicle_type
|
||||||
|
}, indent=4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self.make_request('post', self.generate_ewaybill_url, headers, data)
|
||||||
|
if res.get('success'):
|
||||||
|
self.invoice.ewaybill = res.get('result').get('EwbNo')
|
||||||
|
self.invoice.eway_bill_cancelled = 0
|
||||||
|
self.invoice.update(args)
|
||||||
|
self.invoice.flags.updater_reference = {
|
||||||
|
'doctype': self.invoice.doctype,
|
||||||
|
'docname': self.invoice.name,
|
||||||
|
'label': _('E-Way Bill Generated')
|
||||||
|
}
|
||||||
|
self.update_invoice()
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RequestFailed
|
||||||
|
|
||||||
|
except RequestFailed:
|
||||||
|
errors = self.sanitize_error_message(res.get('message'))
|
||||||
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.log_error(data)
|
||||||
|
self.raise_error(True)
|
||||||
|
|
||||||
|
def cancel_eway_bill(self, eway_bill, reason, remark):
|
||||||
|
headers = self.get_headers()
|
||||||
|
data = json.dumps({
|
||||||
|
'ewbNo': eway_bill,
|
||||||
|
'cancelRsnCode': reason,
|
||||||
|
'cancelRmrk': remark
|
||||||
|
}, indent=4)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
|
||||||
|
if res.get('success'):
|
||||||
|
self.invoice.ewaybill = ''
|
||||||
|
self.invoice.eway_bill_cancelled = 1
|
||||||
|
self.invoice.flags.updater_reference = {
|
||||||
|
'doctype': self.invoice.doctype,
|
||||||
|
'docname': self.invoice.name,
|
||||||
|
'label': _('E-Way Bill Cancelled - {}').format(remark)
|
||||||
|
}
|
||||||
|
self.update_invoice()
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RequestFailed
|
||||||
|
|
||||||
|
except RequestFailed:
|
||||||
|
errors = self.sanitize_error_message(res.get('message'))
|
||||||
|
self.raise_error(errors=errors)
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
self.log_error(data)
|
||||||
|
self.raise_error(True)
|
||||||
|
|
||||||
|
def sanitize_error_message(self, message):
|
||||||
|
'''
|
||||||
|
On validation errors, response message looks something like this:
|
||||||
|
message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable,
|
||||||
|
3095 : Supplier GSTIN is inactive'
|
||||||
|
we search for string between ':' to extract the error messages
|
||||||
|
errors = [
|
||||||
|
': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ',
|
||||||
|
': Test'
|
||||||
|
]
|
||||||
|
then we trim down the message by looping over errors
|
||||||
|
'''
|
||||||
|
errors = re.findall(': [^:]+', message)
|
||||||
|
for idx, e in enumerate(errors):
|
||||||
|
# remove colons
|
||||||
|
errors[idx] = errors[idx].replace(':', '').strip()
|
||||||
|
# if not last
|
||||||
|
if idx != len(errors) - 1:
|
||||||
|
# remove last 7 chars eg: ', 3095 '
|
||||||
|
errors[idx] = errors[idx][:-6]
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
def log_error(self, data={}):
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
data = json.loads(data)
|
||||||
|
|
||||||
|
seperator = "--" * 50
|
||||||
|
err_tb = traceback.format_exc()
|
||||||
|
err_msg = str(sys.exc_info()[1])
|
||||||
|
data = json.dumps(data, indent=4)
|
||||||
|
|
||||||
|
message = "\n".join([
|
||||||
|
"Error", err_msg, seperator,
|
||||||
|
"Data:", data, seperator,
|
||||||
|
"Exception:", err_tb
|
||||||
|
])
|
||||||
|
frappe.log_error(title=_('E Invoice Request Failed'), message=message)
|
||||||
|
|
||||||
|
def raise_error(self, raise_exception=False, errors=[]):
|
||||||
|
title = _('E Invoice Request Failed')
|
||||||
|
if errors:
|
||||||
|
frappe.throw(errors, title=title, as_list=1)
|
||||||
|
else:
|
||||||
|
link_to_error_list = '<a href="desk#List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
|
||||||
|
frappe.msgprint(
|
||||||
|
_('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list),
|
||||||
|
title=title,
|
||||||
|
raise_exception=raise_exception,
|
||||||
|
indicator='red'
|
||||||
|
)
|
||||||
|
|
||||||
|
def set_einvoice_data(self, res):
|
||||||
|
enc_signed_invoice = res.get('SignedInvoice')
|
||||||
|
dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data']
|
||||||
|
|
||||||
|
self.invoice.irn = res.get('Irn')
|
||||||
|
self.invoice.ewaybill = res.get('EwbNo')
|
||||||
|
self.invoice.signed_einvoice = dec_signed_invoice
|
||||||
|
self.invoice.signed_qr_code = res.get('SignedQRCode')
|
||||||
|
|
||||||
|
self.attach_qrcode_image()
|
||||||
|
|
||||||
|
self.invoice.flags.updater_reference = {
|
||||||
|
'doctype': self.invoice.doctype,
|
||||||
|
'docname': self.invoice.name,
|
||||||
|
'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
|
||||||
|
|
||||||
|
_file = frappe.new_doc('File')
|
||||||
|
_file.update({
|
||||||
|
'file_name': f'QRCode_{docname}.png',
|
||||||
|
'attached_to_doctype': doctype,
|
||||||
|
'attached_to_name': docname,
|
||||||
|
'content': 'qrcode',
|
||||||
|
'is_private': 1
|
||||||
|
})
|
||||||
|
_file.insert()
|
||||||
|
frappe.db.commit()
|
||||||
|
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)
|
||||||
|
|
||||||
|
self.invoice.qrcode_image = _file.file_url
|
||||||
|
|
||||||
|
def update_invoice(self):
|
||||||
|
self.invoice.flags.ignore_validate_update_after_submit = True
|
||||||
|
self.invoice.flags.ignore_validate = True
|
||||||
|
self.invoice.save()
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_einvoice(doctype, docname):
|
||||||
|
invoice = frappe.get_doc(doctype, docname)
|
||||||
|
return make_einvoice(invoice)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def generate_irn(doctype, docname):
|
||||||
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
gsp_connector.generate_irn()
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def cancel_irn(doctype, docname, irn, reason, remark):
|
||||||
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
gsp_connector.cancel_irn(irn, reason, remark)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def generate_eway_bill(doctype, docname, **kwargs):
|
||||||
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
gsp_connector.generate_eway_bill(**kwargs)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
|
||||||
|
gsp_connector = GSPConnector(doctype, docname)
|
||||||
|
gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
@ -87,7 +87,7 @@ def add_custom_roles_for_reports():
|
|||||||
)).insert()
|
)).insert()
|
||||||
|
|
||||||
def add_permissions():
|
def add_permissions():
|
||||||
for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'):
|
for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'):
|
||||||
add_permission(doctype, 'All', 0)
|
add_permission(doctype, 'All', 0)
|
||||||
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
|
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
|
||||||
add_permission(doctype, role, 0)
|
add_permission(doctype, role, 0)
|
||||||
@ -103,9 +103,10 @@ def add_permissions():
|
|||||||
def add_print_formats():
|
def add_print_formats():
|
||||||
frappe.reload_doc("regional", "print_format", "gst_tax_invoice")
|
frappe.reload_doc("regional", "print_format", "gst_tax_invoice")
|
||||||
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
|
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
|
||||||
|
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
|
||||||
|
|
||||||
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
|
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
|
||||||
name in('GST POS Invoice', 'GST Tax Invoice') """)
|
name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
|
||||||
|
|
||||||
def make_custom_fields(update=True):
|
def make_custom_fields(update=True):
|
||||||
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
|
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
|
||||||
@ -351,7 +352,6 @@ def make_custom_fields(update=True):
|
|||||||
'label': 'Mode of Transport',
|
'label': 'Mode of Transport',
|
||||||
'fieldtype': 'Select',
|
'fieldtype': 'Select',
|
||||||
'options': '\nRoad\nAir\nRail\nShip',
|
'options': '\nRoad\nAir\nRail\nShip',
|
||||||
'default': 'Road',
|
|
||||||
'insert_after': 'transporter_name',
|
'insert_after': 'transporter_name',
|
||||||
'print_hide': 1,
|
'print_hide': 1,
|
||||||
'translatable': 0
|
'translatable': 0
|
||||||
@ -388,13 +388,34 @@ def make_custom_fields(update=True):
|
|||||||
'fieldname': 'ewaybill',
|
'fieldname': 'ewaybill',
|
||||||
'label': 'E-Way Bill No.',
|
'label': 'E-Way Bill No.',
|
||||||
'fieldtype': 'Data',
|
'fieldtype': 'Data',
|
||||||
'depends_on': 'eval:(doc.docstatus === 1)',
|
'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)',
|
||||||
'allow_on_submit': 1,
|
'allow_on_submit': 1,
|
||||||
'insert_after': 'tax_id',
|
'insert_after': 'tax_id',
|
||||||
'translatable': 0
|
'translatable': 0
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
si_einvoice_fields = [
|
||||||
|
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
|
||||||
|
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
|
||||||
|
|
||||||
|
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
|
||||||
|
|
||||||
|
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
||||||
|
|
||||||
|
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||||
|
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
|
||||||
|
|
||||||
|
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
|
||||||
|
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
|
||||||
|
|
||||||
|
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
|
||||||
|
|
||||||
|
dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
|
||||||
|
|
||||||
|
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
|
||||||
|
]
|
||||||
|
|
||||||
custom_fields = {
|
custom_fields = {
|
||||||
'Address': [
|
'Address': [
|
||||||
dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data',
|
dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data',
|
||||||
@ -407,7 +428,7 @@ def make_custom_fields(update=True):
|
|||||||
'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields,
|
'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields,
|
||||||
'Purchase Order': purchase_invoice_gst_fields,
|
'Purchase Order': purchase_invoice_gst_fields,
|
||||||
'Purchase Receipt': purchase_invoice_gst_fields,
|
'Purchase Receipt': purchase_invoice_gst_fields,
|
||||||
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields,
|
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
|
||||||
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
|
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
|
||||||
'Sales Order': sales_invoice_gst_fields,
|
'Sales Order': sales_invoice_gst_fields,
|
||||||
'Tax Category': inter_state_gst_field,
|
'Tax Category': inter_state_gst_field,
|
||||||
|
@ -12,6 +12,9 @@ erpnext.setup_auto_gst_taxation = (doctype) => {
|
|||||||
tax_category: function(frm) {
|
tax_category: function(frm) {
|
||||||
frm.trigger('get_tax_template');
|
frm.trigger('get_tax_template');
|
||||||
},
|
},
|
||||||
|
customer_address: function(frm) {
|
||||||
|
frm.trigger('get_tax_template');
|
||||||
|
},
|
||||||
get_tax_template: function(frm) {
|
get_tax_template: function(frm) {
|
||||||
if (!frm.doc.company) return;
|
if (!frm.doc.company) return;
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ def validate_gstin_for_india(doc, method):
|
|||||||
.format(doc.gst_state_number))
|
.format(doc.gst_state_number))
|
||||||
|
|
||||||
def validate_tax_category(doc, method):
|
def validate_tax_category(doc, method):
|
||||||
if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
|
if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
|
||||||
if doc.is_inter_state:
|
if doc.is_inter_state:
|
||||||
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
|
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
|
||||||
else:
|
else:
|
||||||
|
@ -14,7 +14,6 @@ from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty
|
|||||||
from frappe.desk.notifications import clear_doctype_notifications
|
from frappe.desk.notifications import clear_doctype_notifications
|
||||||
from frappe.contacts.doctype.address.address import get_company_address
|
from frappe.contacts.doctype.address.address import get_company_address
|
||||||
from erpnext.controllers.selling_controller import SellingController
|
from erpnext.controllers.selling_controller import SellingController
|
||||||
from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date
|
|
||||||
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
from erpnext.selling.doctype.customer.customer import check_credit_limit
|
||||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||||
@ -418,8 +417,7 @@ class SalesOrder(SellingController):
|
|||||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||||
|
|
||||||
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
|
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
|
||||||
delivery_date = get_next_schedule_date(ref_doc_delivery_date,
|
delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
|
||||||
auto_repeat_doc.frequency, auto_repeat_doc.start_date, cint(auto_repeat_doc.repeat_on_day))
|
|
||||||
|
|
||||||
if delivery_date <= transaction_date:
|
if delivery_date <= transaction_date:
|
||||||
delivery_date_diff = frappe.utils.date_diff(ref_doc_delivery_date, red_doc_transaction_date)
|
delivery_date_diff = frappe.utils.date_diff(ref_doc_delivery_date, red_doc_transaction_date)
|
||||||
|
@ -785,7 +785,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-05-29 20:54:32.309460",
|
"modified": "2020-012-07 20:54:32.309460",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Selling",
|
"module": "Selling",
|
||||||
"name": "Sales Order Item",
|
"name": "Sales Order Item",
|
||||||
|
@ -28,7 +28,7 @@ def delete_company_transactions(company_name):
|
|||||||
"Party Account", "Employee", "Sales Taxes and Charges Template",
|
"Party Account", "Employee", "Sales Taxes and Charges Template",
|
||||||
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
|
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
|
||||||
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
|
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
|
||||||
"Item Default"):
|
"Item Default", "Customer", "Supplier"):
|
||||||
delete_for_doctype(doctype, company_name)
|
delete_for_doctype(doctype, company_name)
|
||||||
|
|
||||||
# reset company values
|
# reset company values
|
||||||
|
@ -7,7 +7,8 @@
|
|||||||
"doctype": "Company",
|
"doctype": "Company",
|
||||||
"domain": "Manufacturing",
|
"domain": "Manufacturing",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
"default_holiday_list": "_Test Holiday List"
|
"default_holiday_list": "_Test Holiday List",
|
||||||
|
"enable_perpetual_inventory": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"abbr": "_TC1",
|
"abbr": "_TC1",
|
||||||
@ -17,7 +18,8 @@
|
|||||||
"doctype": "Company",
|
"doctype": "Company",
|
||||||
"domain": "Retail",
|
"domain": "Retail",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
"default_holiday_list": "_Test Holiday List"
|
"default_holiday_list": "_Test Holiday List",
|
||||||
|
"enable_perpetual_inventory": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"abbr": "_TC2",
|
"abbr": "_TC2",
|
||||||
@ -27,7 +29,8 @@
|
|||||||
"doctype": "Company",
|
"doctype": "Company",
|
||||||
"domain": "Retail",
|
"domain": "Retail",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
"default_holiday_list": "_Test Holiday List"
|
"default_holiday_list": "_Test Holiday List",
|
||||||
|
"enable_perpetual_inventory": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"abbr": "_TC3",
|
"abbr": "_TC3",
|
||||||
@ -38,7 +41,8 @@
|
|||||||
"doctype": "Company",
|
"doctype": "Company",
|
||||||
"domain": "Manufacturing",
|
"domain": "Manufacturing",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
"default_holiday_list": "_Test Holiday List"
|
"default_holiday_list": "_Test Holiday List",
|
||||||
|
"enable_perpetual_inventory": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"abbr": "_TC4",
|
"abbr": "_TC4",
|
||||||
@ -50,7 +54,8 @@
|
|||||||
"doctype": "Company",
|
"doctype": "Company",
|
||||||
"domain": "Manufacturing",
|
"domain": "Manufacturing",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
"default_holiday_list": "_Test Holiday List"
|
"default_holiday_list": "_Test Holiday List",
|
||||||
|
"enable_perpetual_inventory": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"abbr": "_TC5",
|
"abbr": "_TC5",
|
||||||
@ -61,7 +66,8 @@
|
|||||||
"doctype": "Company",
|
"doctype": "Company",
|
||||||
"domain": "Manufacturing",
|
"domain": "Manufacturing",
|
||||||
"chart_of_accounts": "Standard",
|
"chart_of_accounts": "Standard",
|
||||||
"default_holiday_list": "_Test Holiday List"
|
"default_holiday_list": "_Test Holiday List",
|
||||||
|
"enable_perpetual_inventory": 0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"abbr": "TCP1",
|
"abbr": "TCP1",
|
||||||
|
@ -8,13 +8,8 @@ import unittest
|
|||||||
|
|
||||||
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no
|
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
|
|
||||||
|
|
||||||
class TestBatch(unittest.TestCase):
|
class TestBatch(unittest.TestCase):
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
set_perpetual_inventory(0)
|
|
||||||
|
|
||||||
def test_item_has_batch_enabled(self):
|
def test_item_has_batch_enabled(self):
|
||||||
self.assertRaises(ValidationError, frappe.get_doc({
|
self.assertRaises(ValidationError, frappe.get_doc({
|
||||||
"doctype": "Batch",
|
"doctype": "Batch",
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user