Merge branch 'version-13-hotfix' into datev_more_info

This commit is contained in:
barredterra
2021-08-03 10:47:57 +02:00
99 changed files with 1276 additions and 2222 deletions

View File

@@ -8,18 +8,3 @@ rules:
dynamic content. Avoid it or use safe_eval(). dynamic content. Avoid it or use safe_eval().
languages: [python] languages: [python]
severity: ERROR severity: ERROR
- id: frappe-sqli-format-strings
patterns:
- pattern-inside: |
@frappe.whitelist()
def $FUNC(...):
...
- pattern-either:
- pattern: frappe.db.sql("..." % ...)
- pattern: frappe.db.sql(f"...", ...)
- pattern: frappe.db.sql("...".format(...), ...)
message: |
Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
languages: [python]
severity: WARNING

View File

@@ -1,16 +1,25 @@
name: Backport name: Backport
on: on:
pull_request: pull_request_target:
types: types:
- closed - closed
- labeled - labeled
jobs: jobs:
backport: main:
runs-on: ubuntu-18.04 runs-on: ubuntu-latest
name: Backport
steps: steps:
- name: Backport - name: Checkout Actions
uses: tibdex/backport@v1 uses: actions/checkout@v2
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} repository: "frappe/backport"
path: ./actions
ref: develop
- name: Install Actions
run: npm install --production --prefix ./actions
- name: Run backport
uses: ./actions/backport
with:
token: ${{secrets.BACKPORT_BOT_TOKEN}}
labelsToAdd: "backport"
title: "{{originalTitle}}"

View File

@@ -21,13 +21,13 @@ erpnext/quality_management/ @marination @rohitwaghchaure
erpnext/shopping_cart/ @marination erpnext/shopping_cart/ @marination
erpnext/stock/ @marination @rohitwaghchaure @ankush erpnext/stock/ @marination @rohitwaghchaure @ankush
erpnext/crm/ @ruchamahabal erpnext/crm/ @ruchamahabal @pateljannat
erpnext/education/ @ruchamahabal erpnext/education/ @ruchamahabal @pateljannat
erpnext/healthcare/ @ruchamahabal erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
erpnext/hr/ @ruchamahabal erpnext/hr/ @ruchamahabal @pateljannat
erpnext/non_profit/ @ruchamahabal erpnext/non_profit/ @ruchamahabal
erpnext/payroll @ruchamahabal erpnext/payroll @ruchamahabal @pateljannat
erpnext/projects/ @ruchamahabal erpnext/projects/ @ruchamahabal @pateljannat
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination

View File

@@ -27,6 +27,9 @@ class ExchangeRateRevaluation(Document):
if not (self.company and self.posting_date): if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries")) frappe.throw(_("Please select Company and Posting Date to getting entries"))
def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry')
@frappe.whitelist() @frappe.whitelist()
def check_journal_entry_condition(self): def check_journal_entry_condition(self):
total_debit = frappe.db.get_value("Journal Entry Account", { total_debit = frappe.db.get_value("Journal Entry Account", {
@@ -99,10 +102,12 @@ class ExchangeRateRevaluation(Document):
sum(debit) - sum(credit) as balance sum(debit) - sum(credit) as balance
from `tabGL Entry` from `tabGL Entry`
where account in (%s) where account in (%s)
group by account, party_type, party and posting_date <= %s
and is_cancelled = 0
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit) having sum(debit) != sum(credit)
order by account order by account
""" % ', '.join(['%s']*len(accounts)), tuple(accounts), as_dict=1) """ % (', '.join(['%s']*len(accounts)), '%s'), tuple(accounts + [self.posting_date]), as_dict=1)
return account_details return account_details
@@ -143,9 +148,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"), "party_type": d.get("party_type"),
"party": d.get("party"), "party": d.get("party"),
"account_currency": d.get("account_currency"), "account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"), "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
dr_or_cr: abs(d.get("balance_in_account_currency")), dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate":d.get("new_exchange_rate"), "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name, "reference_name": self.name,
}) })
@@ -154,9 +159,9 @@ class ExchangeRateRevaluation(Document):
"party_type": d.get("party_type"), "party_type": d.get("party_type"),
"party": d.get("party"), "party": d.get("party"),
"account_currency": d.get("account_currency"), "account_currency": d.get("account_currency"),
"balance": d.get("balance_in_account_currency"), "balance": flt(d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")),
reverse_dr_or_cr: abs(d.get("balance_in_account_currency")), reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
"exchange_rate": d.get("current_exchange_rate"), "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation", "reference_type": "Exchange Rate Revaluation",
"reference_name": self.name "reference_name": self.name
}) })
@@ -185,9 +190,9 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
account_details = {} account_details = {}
company_currency = erpnext.get_company_currency(company) company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(account, party_type=party_type, party=party, in_account_currency=False) balance = get_balance_on(account, date=posting_date, party_type=party_type, party=party, in_account_currency=False)
if balance: if balance:
balance_in_account_currency = get_balance_on(account, party_type=party_type, party=party) balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0 current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date) new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate

View File

@@ -7,6 +7,8 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', { frappe.ui.form.on('Payment Entry', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
if(frm.doc.__islocal) { if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null); if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null); if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);

View File

@@ -306,5 +306,5 @@ def reconcile_dr_cr_note(dr_cr_notes, company):
} }
] ]
}) })
jv.flags.ignore_mandatory = True
jv.submit() jv.submit()

View File

@@ -1545,6 +1545,7 @@
"fieldname": "consolidated_invoice", "fieldname": "consolidated_invoice",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Consolidated Sales Invoice", "label": "Consolidated Sales Invoice",
"no_copy": 1,
"options": "Sales Invoice", "options": "Sales Invoice",
"read_only": 1 "read_only": 1
} }
@@ -1552,7 +1553,7 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-02-01 15:03:33.800707", "modified": "2021-07-29 13:37:20.636171",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",

View File

@@ -168,7 +168,7 @@ 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"]: if parenttype in ["Customer Group", "Item Group", "Territory"]:
parent_field = "parent_{0}".format(frappe.scrub(parenttype)) parent_field = "parent_{0}".format(frappe.scrub(parenttype))

View File

@@ -997,8 +997,8 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_gle = [ expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 37500.0], ["_Test Account Cost for Goods Sold - _TC", 37500.0],
["_Test Payable USD - _TC", -40000.0], ["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", 2500.0] ["Exchange Gain/Loss - _TC", -2500.0]
] ]
gl_entries = frappe.db.sql(""" gl_entries = frappe.db.sql("""
@@ -1028,8 +1028,8 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_gle = [ expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 36500.0], ["_Test Account Cost for Goods Sold - _TC", 36500.0],
["_Test Payable USD - _TC", -38000.0], ["_Test Payable USD - _TC", -35000.0],
["Exchange Gain/Loss - _TC", 1500.0] ["Exchange Gain/Loss - _TC", -1500.0]
] ]
gl_entries = frappe.db.sql(""" gl_entries = frappe.db.sql("""

View File

@@ -48,6 +48,8 @@
"shipping_address", "shipping_address",
"company_address", "company_address",
"company_address_display", "company_address_display",
"dispatch_address_name",
"dispatch_address",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@@ -1966,6 +1968,21 @@
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address_name",
"fieldtype": "Link",
"label": "Dispatch Address Name",
"options": "Address",
"print_hide": 1
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address",
"fieldtype": "Small Text",
"label": "Dispatch Address",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@@ -1978,7 +1995,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-05-20 22:48:33.988881", "modified": "2021-07-08 14:03:55.502522",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -1908,6 +1908,8 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['sgstValue'], 5400) self.assertEqual(data['billLists'][0]['sgstValue'], 5400)
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)
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
self.assertEqual(data['billLists'][0]['fromStateCode'],27)
def test_einvoice_submission_without_irn(self): def test_einvoice_submission_without_irn(self):
# init # init
@@ -2062,6 +2064,30 @@ def make_test_address_for_ewaybill():
address.save() address.save()
if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Dispatch Address Line 1",
"address_title": "_Test Dispatch-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 0,
"phone": "+910000000000",
"gstin": "07AAACC1206D1ZI",
"gst_state": "Delhi",
"gst_state_number": "07",
"pincode": "1100101"
}).insert()
address.append("links", {
"link_doctype": "Company",
"link_name": "_Test Company"
})
address.save()
def make_test_transporter_for_ewaybill(): def make_test_transporter_for_ewaybill():
if not frappe.db.exists('Supplier', '_Test Transporter'): if not frappe.db.exists('Supplier', '_Test Transporter'):
frappe.get_doc({ frappe.get_doc({
@@ -2100,6 +2126,7 @@ def make_sales_invoice_for_ewaybill():
si.distance = 2000 si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing" si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping" si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234" si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular" si.gst_category = "Registered Regular"
si.mode_of_transport = 'Road' si.mode_of_transport = 'Road'

View File

@@ -27,7 +27,8 @@
"base_tax_amount", "base_tax_amount",
"base_total", "base_total",
"base_tax_amount_after_discount_amount", "base_tax_amount_after_discount_amount",
"item_wise_tax_detail" "item_wise_tax_detail",
"dont_recompute_tax"
], ],
"fields": [ "fields": [
{ {
@@ -200,13 +201,22 @@
"fieldname": "included_in_paid_amount", "fieldname": "included_in_paid_amount",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Considered In Paid Amount" "label": "Considered In Paid Amount"
},
{
"default": "0",
"fieldname": "dont_recompute_tax",
"fieldtype": "Check",
"hidden": 1,
"label": "Dont Recompute tax",
"print_hide": 1,
"read_only": 1
} }
], ],
"idx": 1, "idx": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-14 01:44:36.899147", "modified": "2021-07-27 12:40:59.051803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Taxes and Charges", "name": "Sales Taxes and Charges",

View File

@@ -1,263 +1,151 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "allow_import": 1,
"allow_import": 1, "allow_rename": 1,
"allow_rename": 1, "autoname": "Prompt",
"autoname": "Prompt", "creation": "2018-04-13 18:42:06.431683",
"beta": 0, "doctype": "DocType",
"creation": "2018-04-13 18:42:06.431683", "editable_grid": 1,
"custom": 0, "engine": "InnoDB",
"docstatus": 0, "field_order": [
"doctype": "DocType", "category_details_section",
"document_type": "", "category_name",
"editable_grid": 1, "round_off_tax_amount",
"engine": "InnoDB", "column_break_2",
"consider_party_ledger_amount",
"tax_on_excess_amount",
"section_break_8",
"rates",
"section_break_7",
"accounts"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "category_name", "fieldname": "category_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Category Name", "label": "Category Name",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0, },
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_8", "fieldname": "section_break_8",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Tax Withholding Rates", "label": "Tax Withholding Rates",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "rates", "fieldname": "rates",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Rates", "label": "Rates",
"length": 0,
"no_copy": 0,
"options": "Tax Withholding Rate", "options": "Tax Withholding Rate",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "show_days": 1,
"set_only_once": 0, "show_seconds": 1
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "fieldname": "section_break_7",
"allow_in_quick_entry": 0, "fieldtype": "Section Break",
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Account Details", "label": "Account Details",
"length": 0, "show_days": 1,
"no_copy": 0, "show_seconds": 1
"permlevel": 0, },
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "accounts",
"allow_in_quick_entry": 0, "fieldtype": "Table",
"allow_on_submit": 0, "label": "Accounts",
"bold": 0, "options": "Tax Withholding Account",
"collapsible": 0, "reqd": 1,
"columns": 0, "show_days": 1,
"fieldname": "accounts", "show_seconds": 1
"fieldtype": "Table", },
"hidden": 0, {
"ignore_user_permissions": 0, "fieldname": "category_details_section",
"ignore_xss_filter": 0, "fieldtype": "Section Break",
"in_filter": 0, "label": "Category Details",
"in_global_search": 0, "show_days": 1,
"in_list_view": 0, "show_seconds": 1
"in_standard_filter": 0, },
"label": "Accounts", {
"length": 0, "fieldname": "column_break_2",
"no_copy": 0, "fieldtype": "Column Break",
"options": "Tax Withholding Account", "show_days": 1,
"permlevel": 0, "show_seconds": 1
"precision": "", },
"print_hide": 0, {
"print_hide_if_no_value": 0, "default": "0",
"read_only": 0, "description": "Even invoices with apply tax withholding unchecked will be considered for checking cumulative threshold breach",
"remember_last_selected_value": 0, "fieldname": "consider_party_ledger_amount",
"report_hide": 0, "fieldtype": "Check",
"reqd": 1, "label": "Consider Entire Party Ledger Amount",
"search_index": 0, "show_days": 1,
"set_only_once": 0, "show_seconds": 1
"translatable": 0, },
"unique": 0 {
"default": "0",
"description": "Tax will be withheld only for amount exceeding the cumulative threshold",
"fieldname": "tax_on_excess_amount",
"fieldtype": "Check",
"label": "Only Deduct Tax On Excess Amount ",
"show_days": 1,
"show_seconds": 1
},
{
"description": "Checking this will round off the tax amount to the nearest integer",
"fieldname": "round_off_tax_amount",
"fieldtype": "Check",
"label": "Round Off Tax Amount",
"show_days": 1,
"show_seconds": 1
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2021-07-27 21:47:34.396071",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Accounts",
"in_create": 0, "name": "Tax Withholding Category",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-17 22:53:26.193179",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Category",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "System Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "Accounts Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "Accounts User",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "sort_field": "modified",
"read_only": 0, "sort_order": "DESC",
"read_only_onload": 0, "track_changes": 1
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
} }

View File

@@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, getdate from frappe.utils import flt, getdate, cint
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document): class TaxWithholdingCategory(Document):
@@ -86,7 +86,10 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
"rate": tax_rate_detail.tax_withholding_rate, "rate": tax_rate_detail.tax_withholding_rate,
"threshold": tax_rate_detail.single_threshold, "threshold": tax_rate_detail.single_threshold,
"cumulative_threshold": tax_rate_detail.cumulative_threshold, "cumulative_threshold": tax_rate_detail.cumulative_threshold,
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
"consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount,
"tax_on_excess_amount": tax_withholding.tax_on_excess_amount,
"round_off_tax_amount": tax_withholding.round_off_tax_amount
}) })
def get_tax_withholding_rates(tax_withholding, fiscal_year): def get_tax_withholding_rates(tax_withholding, fiscal_year):
@@ -145,6 +148,7 @@ def get_lower_deduction_certificate(fiscal_year, pan_no):
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0] fiscal_year = fiscal_year_details[0]
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers taxable_vouchers = vouchers + advance_vouchers
@@ -235,10 +239,18 @@ def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
tds_amount = 0 tds_amount = 0
invoice_filters = {
'name': ('in', vouchers),
'docstatus': 1
}
supp_credit_amt = frappe.db.get_value('Purchase Invoice', { field = 'sum(net_total)'
'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
}, 'sum(net_total)') or 0.0 if not cint(tax_details.consider_party_ledger_amount):
invoice_filters.update({'apply_tds': 1})
field = 'sum(grand_total)'
supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0
supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
'parent': ('in', vouchers), 'docstatus': 1, 'parent': ('in', vouchers), 'docstatus': 1,
@@ -255,6 +267,13 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
cumulative_threshold = tax_details.get('cumulative_threshold', 0) cumulative_threshold = tax_details.get('cumulative_threshold', 0)
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount):
# Get net total again as TDS is calculated on net total
# Grand is used to just check for threshold breach
net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0
net_total += inv.net_total
supp_credit_amt = net_total - cumulative_threshold
if ldc and is_valid_certificate( if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto, ldc.valid_from, ldc.valid_upto,
inv.get('posting_date') or inv.get('transaction_date'), tax_deducted, inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
@@ -263,6 +282,9 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
else: else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
if cint(tax_details.round_off_tax_amount):
tds_amount = round(tds_amount)
return tds_amount return tds_amount

View File

@@ -87,6 +87,31 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() d.cancel()
def test_tax_withholding_category_checks(self):
invoices = []
frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category")
# First Invoice with no tds check
pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True)
pi.apply_tds = 0
pi.save()
pi.submit()
invoices.append(pi)
# Second Invoice will apply TDS checked
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000)
pi1.submit()
invoices.append(pi1)
# Cumulative threshold is 30000
# Threshold calculation should be on both the invoices
# TDS should be applied only on 1000
self.assertEqual(pi1.taxes[0].tax_amount, 1000)
for d in invoices:
d.cancel()
def test_cumulative_threshold_tcs(self): def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = [] invoices = []
@@ -195,7 +220,7 @@ def create_sales_invoice(**args):
def create_records(): def create_records():
# create a new suppliers # create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3']:
if frappe.db.exists('Supplier', name): if frappe.db.exists('Supplier', name):
continue continue
@@ -311,3 +336,23 @@ def create_tax_with_holding_category():
'account': 'TDS - _TC' 'account': 'TDS - _TC'
}] }]
}).insert() }).insert()
if not frappe.db.exists("Tax Withholding Category", "New TDS Category"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "New TDS Category",
"category_name": "New TDS Category",
"round_off_tax_amount": 1,
"consider_party_ledger_amount": 1,
"tax_on_excess_amount": 1,
"rates": [{
'fiscal_year': fiscal_year,
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000
}],
"accounts": [{
'company': '_Test Company',
'account': 'TDS - _TC'
}]
}).insert()

View File

@@ -99,7 +99,6 @@ class ReceivablePayableReport(object):
voucher_no = gle.voucher_no, voucher_no = gle.voucher_no,
party = gle.party, party = gle.party,
posting_date = gle.posting_date, posting_date = gle.posting_date,
remarks = gle.remarks,
account_currency = gle.account_currency, account_currency = gle.account_currency,
invoiced = 0.0, invoiced = 0.0,
paid = 0.0, paid = 0.0,
@@ -579,7 +578,7 @@ class ReceivablePayableReport(object):
self.gl_entries = frappe.db.sql(""" self.gl_entries = frappe.db.sql("""
select select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, remarks, {0} against_voucher_type, against_voucher, account_currency, {0}
from from
`tabGL Entry` `tabGL Entry`
where where
@@ -792,8 +791,6 @@ class ReceivablePayableReport(object):
self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link',
options='Supplier Group') options='Supplier Group')
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200)
def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120): def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120):
if not fieldname: fieldname = scrub(label) if not fieldname: fieldname = scrub(label)
if fieldtype=='Currency': options='currency' if fieldtype=='Currency': options='currency'

View File

@@ -241,6 +241,7 @@ class GrossProfitGenerator(object):
sle.voucher_detail_no == row.item_row: sle.voucher_detail_no == row.item_row:
previous_stock_value = len(my_sle) > i+1 and \ previous_stock_value = len(my_sle) > i+1 and \
flt(my_sle[i+1].stock_value) or 0.0 flt(my_sle[i+1].stock_value) or 0.0
if previous_stock_value: if previous_stock_value:
return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty))
else: else:
@@ -335,7 +336,7 @@ class GrossProfitGenerator(object):
res = frappe.db.sql("""select item_code, voucher_type, voucher_no, res = frappe.db.sql("""select item_code, voucher_type, voucher_no,
voucher_detail_no, stock_value, warehouse, actual_qty as qty voucher_detail_no, stock_value, warehouse, actual_qty as qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where company=%(company)s where company=%(company)s and is_cancelled = 0
order by order by
item_code desc, warehouse desc, posting_date desc, item_code desc, warehouse desc, posting_date desc,
posting_time desc, creation desc""", self.filters, as_dict=True) posting_time desc, creation desc""", self.filters, as_dict=True)

View File

@@ -966,7 +966,7 @@ def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
for e in existing_gle: for e in existing_gle:
if entry.account == e.account: if entry.account == e.account:
account_existed = True account_existed = True
if (entry.account == e.account and entry.against_account == e.against_account if (entry.account == e.account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and ( flt(entry.debit, precision) != flt(e.debit, precision) or and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))): flt(entry.credit, precision) != flt(e.credit, precision))):

View File

@@ -674,19 +674,24 @@ class AccountsController(TransactionBase):
if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']: if self.get('doctype') in ['Purchase Invoice', 'Sales Invoice']:
for d in self.get("advances"): for d in self.get("advances"):
if d.exchange_gain_loss: if d.exchange_gain_loss:
party = self.supplier if self.get('doctype') == 'Purchase Invoice' else self.customer is_purchase_invoice = self.get('doctype') == 'Purchase Invoice'
party_account = self.credit_to if self.get('doctype') == 'Purchase Invoice' else self.debit_to party = self.supplier if is_purchase_invoice else self.customer
party_type = "Supplier" if self.get('doctype') == 'Purchase Invoice' else "Customer" party_account = self.credit_to if is_purchase_invoice else self.debit_to
party_type = "Supplier" if is_purchase_invoice else "Customer"
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account') gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
if not gain_loss_account:
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
.format(self.get('company')))
account_currency = get_account_currency(gain_loss_account) account_currency = get_account_currency(gain_loss_account)
if account_currency != self.company_currency: if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency)) frappe.throw(_("Currency for {0} must be {1}").format(gain_loss_account, self.company_currency))
# for purchase # for purchase
dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit' dr_or_cr = 'debit' if d.exchange_gain_loss > 0 else 'credit'
# just reverse for sales? if not is_purchase_invoice:
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit' # just reverse for sales?
dr_or_cr = 'debit' if dr_or_cr == 'credit' else 'credit'
gl_entries.append( gl_entries.append(
self.get_gl_dict({ self.get_gl_dict({
@@ -1112,8 +1117,11 @@ class AccountsController(TransactionBase):
for d in self.get("payment_schedule"): for d in self.get("payment_schedule"):
if d.invoice_portion: if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount')) d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
def set_due_date(self): def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date] due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]

View File

@@ -407,6 +407,7 @@ def get_batch_no(doctype, txt, searchfield, start, page_len, filters):
INNER JOIN `tabBatch` batch on sle.batch_no = batch.name INNER JOIN `tabBatch` batch on sle.batch_no = batch.name
where where
batch.disabled = 0 batch.disabled = 0
and sle.is_cancelled = 0
and sle.item_code = %(item_code)s and sle.item_code = %(item_code)s
and sle.warehouse = %(warehouse)s and sle.warehouse = %(warehouse)s
and (sle.batch_no like %(txt)s and (sle.batch_no like %(txt)s

View File

@@ -53,12 +53,17 @@ class StockController(AccountsController):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
for d in self.get("items"): for d in self.get("items"):
if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no: if hasattr(d, 'serial_no') and hasattr(d, 'batch_no') and d.serial_no and d.batch_no:
serial_nos = get_serial_nos(d.serial_no) serial_nos = frappe.get_all("Serial No",
for serial_no_data in frappe.get_all("Serial No", fields=["batch_no", "name", "warehouse"],
filters={"name": ("in", serial_nos)}, fields=["batch_no", "name"]): filters={
if serial_no_data.batch_no != d.batch_no: "name": ("in", get_serial_nos(d.serial_no))
}
)
for row in serial_nos:
if row.warehouse and row.batch_no != d.batch_no:
frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}") frappe.throw(_("Row #{0}: Serial No {1} does not belong to Batch {2}")
.format(d.idx, serial_no_data.name, d.batch_no)) .format(d.idx, row.name, d.batch_no))
if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2: if flt(d.qty) > 0.0 and d.get("batch_no") and self.get("posting_date") and self.docstatus < 2:
expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date") expiry_date = frappe.get_cached_value("Batch", d.get("batch_no"), "expiry_date")

View File

@@ -152,7 +152,7 @@ class calculate_taxes_and_totals(object):
validate_taxes_and_charges(tax) validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, self.doc) validate_inclusive_tax(tax, self.doc)
if not self.doc.get('is_consolidated'): if not (self.doc.get('is_consolidated') or tax.get("dont_recompute_tax")):
tax.item_wise_tax_detail = {} tax.item_wise_tax_detail = {}
tax_fields = ["total", "tax_amount_after_discount_amount", tax_fields = ["total", "tax_amount_after_discount_amount",
@@ -347,7 +347,7 @@ class calculate_taxes_and_totals(object):
elif tax.charge_type == "On Item Quantity": elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty current_tax_amount = tax_rate * item.qty
if not self.doc.get("is_consolidated"): if not (self.doc.get("is_consolidated") or tax.get("dont_recompute_tax")):
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount return current_tax_amount
@@ -455,7 +455,8 @@ class calculate_taxes_and_totals(object):
def _cleanup(self): def _cleanup(self):
if not self.doc.get('is_consolidated'): if not self.doc.get('is_consolidated'):
for tax in self.doc.get("taxes"): for tax in self.doc.get("taxes"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) if not tax.get("dont_recompute_tax"):
tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':'))
def set_discount_amount(self): def set_discount_amount(self):
if self.doc.additional_discount_percentage: if self.doc.additional_discount_percentage:

View File

@@ -34,11 +34,14 @@ def enroll_student(source_name):
} }
}}, ignore_permissions=True) }}, ignore_permissions=True)
student.save() student.save()
student_applicant = frappe.db.get_value("Student Applicant", source_name,
["student_category", "program"], as_dict=True)
program_enrollment = frappe.new_doc("Program Enrollment") program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name program_enrollment.student = student.name
program_enrollment.student_category = student.student_category program_enrollment.student_category = student_applicant.student_category
program_enrollment.student_name = student.title program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program") program_enrollment.program = student_applicant.program
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user) frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
return program_enrollment return program_enrollment

View File

@@ -1,195 +1,68 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2016-06-10 03:29:02.539914",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2016-06-10 03:29:02.539914", "field_order": [
"custom": 0, "student_applicant",
"docstatus": 0, "student",
"doctype": "DocType", "student_name",
"document_type": "", "column_break_3",
"editable_grid": 1, "student_batch_name",
"student_category"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "student_applicant",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Applicant",
"columns": 0, "options": "Student Applicant"
"depends_on": "", },
"fieldname": "student_applicant",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Applicant",
"length": 0,
"no_copy": 0,
"options": "Student Applicant",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student",
"columns": 0, "options": "Student"
"depends_on": "", },
"fieldname": "student",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student",
"length": 0,
"no_copy": 0,
"options": "Student",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "column_break_3",
"allow_on_submit": 0, "fieldtype": "Column Break"
"bold": 0, },
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_name",
"allow_on_submit": 0, "fieldtype": "Data",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Name",
"columns": 0, "read_only": 1
"fieldname": "student_name", },
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "fieldname": "student_batch_name",
"allow_on_submit": 0, "fieldtype": "Link",
"bold": 0, "in_list_view": 1,
"collapsible": 0, "label": "Student Batch Name",
"columns": 0, "options": "Student Batch Name"
"fieldname": "student_batch_name", },
"fieldtype": "Link", {
"hidden": 0, "fieldname": "student_category",
"ignore_user_permissions": 0, "fieldtype": "Link",
"ignore_xss_filter": 0, "label": "Student Category",
"in_filter": 0, "options": "Student Category",
"in_global_search": 0, "read_only": 1
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Student Batch Name",
"length": 0,
"no_copy": 0,
"options": "Student Batch Name",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2021-07-29 18:19:54.471594",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Education",
"in_create": 0, "name": "Program Enrollment Tool Student",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "restrict_to_domain": "Education",
"modified": "2018-01-02 12:03:53.890741", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC"
"module": "Education",
"name": "Program Enrollment Tool Student",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"restrict_to_domain": "Education",
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
} }

View File

@@ -18,5 +18,8 @@ frappe.ui.form.on('Shopify Log', {
}) })
}).addClass('btn-primary'); }).addClass('btn-primary');
} }
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
} }
}); });

View File

@@ -36,6 +36,10 @@ frappe.ui.form.on("Shopify Settings", "refresh", function(frm){
frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note); frm.toggle_reqd("delivery_note_series", frm.doc.sync_delivery_note);
} }
let app_link = "<a href='https://frappecloud.com/marketplace/apps/ecommerce-integrations' target='_blank'>Ecommerce Integrations</a>"
frm.dashboard.add_comment(__("Shopify Integration will be removed from ERPNext in Version 14. Please install {0} app to continue using it.", [app_link]), "yellow", true);
}) })
$.extend(erpnext_integrations.shopify_settings, { $.extend(erpnext_integrations.shopify_settings, {

View File

@@ -24,7 +24,8 @@ doctype_js = {
"Address": "public/js/address.js", "Address": "public/js/address.js",
"Communication": "public/js/communication.js", "Communication": "public/js/communication.js",
"Event": "public/js/event.js", "Event": "public/js/event.js",
"Newsletter": "public/js/newsletter.js" "Newsletter": "public/js/newsletter.js",
"Contact": "public/js/contact.js"
} }
override_doctype_class = { override_doctype_class = {

View File

@@ -9,7 +9,7 @@ from frappe.utils import flt, getdate
from frappe import _ from frappe import _
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name, validate_active_employee
class Appraisal(Document): class Appraisal(Document):
def validate(self): def validate(self):
@@ -19,6 +19,7 @@ class Appraisal(Document):
if not self.goals: if not self.goals:
frappe.throw(_("Goals cannot be empty")) frappe.throw(_("Goals cannot be empty"))
validate_active_employee(self.employee)
set_employee_name(self) set_employee_name(self)
self.validate_dates() self.validate_dates()
self.validate_existing_appraisal() self.validate_existing_appraisal()

View File

@@ -8,11 +8,13 @@ from frappe.utils import getdate, nowdate
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, formatdate from frappe.utils import cstr, get_datetime, formatdate
from erpnext.hr.utils import validate_active_employee
class Attendance(Document): class Attendance(Document):
def validate(self): def validate(self):
from erpnext.controllers.status_updater import validate_status from erpnext.controllers.status_updater import validate_status
validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"]) validate_status(self.status, ["Present", "Absent", "On Leave", "Half Day", "Work From Home"])
validate_active_employee(self.employee)
self.validate_attendance_date() self.validate_attendance_date()
self.validate_duplicate_record() self.validate_duplicate_record()
self.validate_employee_status() self.validate_employee_status()

View File

@@ -8,10 +8,11 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import date_diff, add_days, getdate from frappe.utils import date_diff, add_days, getdate
from erpnext.hr.doctype.employee.employee import is_holiday from erpnext.hr.doctype.employee.employee import is_holiday
from erpnext.hr.utils import validate_dates from erpnext.hr.utils import validate_dates, validate_active_employee
class AttendanceRequest(Document): class AttendanceRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.from_date, self.to_date) validate_dates(self, self.from_date, self.to_date)
if self.half_day: if self.half_day:
if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date): if not getdate(self.from_date)<=getdate(self.half_day_date)<=getdate(self.to_date):

View File

@@ -7,12 +7,13 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import date_diff, add_days, getdate, cint, format_date from frappe.utils import date_diff, add_days, getdate, cint, format_date
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, validate_active_employee, \
get_holidays_for_employee, create_additional_leave_ledger_entry get_holidays_for_employee, create_additional_leave_ledger_entry
class CompensatoryLeaveRequest(Document): class CompensatoryLeaveRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_dates(self, self.work_from_date, self.work_end_date) validate_dates(self, self.work_from_date, self.work_end_date)
if self.half_day: if self.half_day:
if not self.half_day_date: if not self.half_day_date:

View File

@@ -13,8 +13,10 @@ from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
class EmployeeUserDisabledError(frappe.ValidationError): pass class EmployeeUserDisabledError(frappe.ValidationError):
class EmployeeLeftValidationError(frappe.ValidationError): pass pass
class InactiveEmployeeStatusError(frappe.ValidationError):
pass
class Employee(NestedSet): class Employee(NestedSet):
nsm_parent_field = 'reports_to' nsm_parent_field = 'reports_to'
@@ -196,7 +198,7 @@ class Employee(NestedSet):
message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees) message += "<br><br><ul><li>" + "</li><li>".join(link_to_employees)
message += "</li></ul><br>" message += "</li></ul><br>"
message += _("Please make sure the employees above report to another Active employee.") message += _("Please make sure the employees above report to another Active employee.")
throw(message, EmployeeLeftValidationError, _("Cannot Relieve Employee")) throw(message, InactiveEmployeeStatusError, _("Cannot Relieve Employee"))
if not self.relieving_date: if not self.relieving_date:
throw(_("Please enter relieving date.")) throw(_("Please enter relieving date."))

View File

@@ -7,7 +7,7 @@ import frappe
import erpnext import erpnext
import unittest import unittest
import frappe.utils import frappe.utils
from erpnext.hr.doctype.employee.employee import EmployeeLeftValidationError from erpnext.hr.doctype.employee.employee import InactiveEmployeeStatusError
test_records = frappe.get_test_records('Employee') test_records = frappe.get_test_records('Employee')
@@ -45,10 +45,33 @@ class TestEmployee(unittest.TestCase):
employee2_doc.save() employee2_doc.save()
employee1_doc.reload() employee1_doc.reload()
employee1_doc.status = 'Left' employee1_doc.status = 'Left'
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save) self.assertRaises(InactiveEmployeeStatusError, employee1_doc.save)
def test_employee_status_inactive(self):
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
employee = make_employee("test_employee_status@company.com")
employee_doc = frappe.get_doc("Employee", employee)
employee_doc.status = "Inactive"
employee_doc.save()
employee_doc.reload()
make_holiday_list()
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List")
frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""")
salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",
employee=employee_doc.name, company=employee_doc.company)
salary_slip = make_salary_slip(salary_structure.name, employee=employee_doc.name)
self.assertRaises(InactiveEmployeeStatusError, salary_slip.save)
def tearDown(self):
frappe.db.rollback()
def make_employee(user, company=None, **kwargs): def make_employee(user, company=None, **kwargs):
""
if not frappe.db.get_value("User", user): if not frappe.db.get_value("User", user):
frappe.get_doc({ frappe.get_doc({
"doctype": "User", "doctype": "User",
@@ -80,4 +103,5 @@ def make_employee(user, company=None, **kwargs):
employee.insert() employee.insert()
return employee.name return employee.name
else: else:
frappe.db.set_value("Employee", {"employee_name":user}, "status", "Active")
return frappe.get_value("Employee", {"employee_name":user}, "name") return frappe.get_value("Employee", {"employee_name":user}, "name")

View File

@@ -8,6 +8,7 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, nowdate from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
from erpnext.hr.utils import validate_active_employee
class EmployeeAdvanceOverPayment(frappe.ValidationError): class EmployeeAdvanceOverPayment(frappe.ValidationError):
pass pass
@@ -18,11 +19,11 @@ class EmployeeAdvance(Document):
'make_payment_via_journal_entry') 'make_payment_via_journal_entry')
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.set_status() self.set_status()
def on_cancel(self): def on_cancel(self):
self.ignore_linked_doctypes = ('GL Entry') self.ignore_linked_doctypes = ('GL Entry')
self.set_status()
def set_status(self): def set_status(self):
if self.docstatus == 0: if self.docstatus == 0:
@@ -183,9 +184,9 @@ def make_return_entry(employee, company, employee_advance_name, return_amount,
bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment) bank_cash_account = get_default_bank_cash_account(company, account_type='Cash', mode_of_payment = mode_of_payment)
if not bank_cash_account: if not bank_cash_account:
frappe.throw(_("Please set a Default Cash Account in Company defaults")) frappe.throw(_("Please set a Default Cash Account in Company defaults"))
advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency') advance_account_currency = frappe.db.get_value('Account', advance_account, 'account_currency')
je = frappe.new_doc('Journal Entry') je = frappe.new_doc('Journal Entry')
je.posting_date = nowdate() je.posting_date = nowdate()
je.voucher_type = get_voucher_type(mode_of_payment) je.voucher_type = get_voucher_type(mode_of_payment)
@@ -229,4 +230,4 @@ def get_voucher_type(mode_of_payment=None):
if mode_of_payment_type == "Bank": if mode_of_payment_type == "Bank":
voucher_type = "Bank Entry" voucher_type = "Bank Entry"
return voucher_type return voucher_type

View File

@@ -9,9 +9,11 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift from erpnext.hr.doctype.shift_assignment.shift_assignment import get_actual_start_end_datetime_of_shift
from erpnext.hr.utils import validate_active_employee
class EmployeeCheckin(Document): class EmployeeCheckin(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_duplicate_log() self.validate_duplicate_log()
self.fetch_shift() self.fetch_shift()
@@ -122,7 +124,7 @@ def mark_attendance_and_link_log(logs, attendance_status, attendance_date, worki
def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type): def calculate_working_hours(logs, check_in_out_type, working_hours_calc_type):
"""Given a set of logs in chronological order calculates the total working hours based on the parameters. """Given a set of logs in chronological order calculates the total working hours based on the parameters.
Zero is returned for all invalid cases. Zero is returned for all invalid cases.
:param logs: The List of 'Employee Checkin'. :param logs: The List of 'Employee Checkin'.
:param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin' :param check_in_out_type: One of: 'Alternating entries as IN and OUT during the same shift', 'Strictly based on Log Type in Employee Checkin'
:param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out' :param working_hours_calc_type: One of: 'First Check-in and Last Check-out', 'Every Valid Check-in and Check-out'

View File

@@ -7,12 +7,11 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.utils import update_employee from erpnext.hr.utils import update_employee, validate_active_employee
class EmployeePromotion(Document): class EmployeePromotion(Document):
def validate(self): def validate(self):
if frappe.get_value("Employee", self.employee, "status") != "Active": validate_active_employee(self.employee)
frappe.throw(_("Cannot promote Employee with status Left or Inactive"))
def before_submit(self): def before_submit(self):
if getdate(self.promotion_date) > getdate(): if getdate(self.promotion_date) > getdate():

View File

@@ -7,9 +7,11 @@ import frappe
from frappe import _ from frappe import _
from frappe.utils import get_link_to_form from frappe.utils import get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class EmployeeReferral(Document): class EmployeeReferral(Document):
def validate(self): def validate(self):
validate_active_employee(self.referrer)
self.set_full_name() self.set_full_name()
self.set_referral_bonus_payment_status() self.set_referral_bonus_payment_status()

View File

@@ -10,10 +10,6 @@ from frappe.utils import getdate
from erpnext.hr.utils import update_employee from erpnext.hr.utils import update_employee
class EmployeeTransfer(Document): class EmployeeTransfer(Document):
def validate(self):
if frappe.get_value("Employee", self.employee, "status") != "Active":
frappe.throw(_("Cannot transfer Employee with status Left or Inactive"))
def before_submit(self): def before_submit(self):
if getdate(self.transfer_date) > getdate(): if getdate(self.transfer_date) > getdate():
frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"), frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"),

View File

@@ -6,7 +6,7 @@ import frappe, erpnext
from frappe import _ from frappe import _
from frappe.utils import get_fullname, flt, cstr, get_link_to_form from frappe.utils import get_fullname, flt, cstr, get_link_to_form
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import set_employee_name, share_doc_with_approver from erpnext.hr.utils import set_employee_name, share_doc_with_approver, validate_active_employee
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
@@ -23,6 +23,7 @@ class ExpenseClaim(AccountsController):
'make_payment_via_journal_entry') 'make_payment_via_journal_entry')
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_advances() self.validate_advances()
self.validate_sanctioned_amount() self.validate_sanctioned_amount()
self.calculate_total_amount() self.calculate_total_amount()
@@ -35,8 +36,8 @@ class ExpenseClaim(AccountsController):
if self.task and not self.project: if self.task and not self.project:
self.project = frappe.db.get_value("Task", self.task, "project") self.project = frappe.db.get_value("Task", self.task, "project")
def set_status(self): def set_status(self, update=False):
self.status = { status = {
"0": "Draft", "0": "Draft",
"1": "Submitted", "1": "Submitted",
"2": "Cancelled" "2": "Cancelled"
@@ -44,14 +45,18 @@ class ExpenseClaim(AccountsController):
paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount)
precision = self.precision("grand_total") precision = self.precision("grand_total")
if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1
and flt(self.grand_total, precision) == flt(paid_amount, precision))) \ and flt(self.grand_total, precision) == flt(paid_amount, precision))) and self.approval_status == 'Approved':
and self.docstatus == 1 and self.approval_status == 'Approved': status = "Paid"
self.status = "Paid"
elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved':
self.status = "Unpaid" status = "Unpaid"
elif self.docstatus == 1 and self.approval_status == 'Rejected': elif self.docstatus == 1 and self.approval_status == 'Rejected':
self.status = 'Rejected' status = 'Rejected'
if update:
self.db_set("status", status)
else:
self.status = status
def on_update(self): def on_update(self):
share_doc_with_approver(self, self.expense_approver) share_doc_with_approver(self, self.expense_approver)
@@ -74,7 +79,7 @@ class ExpenseClaim(AccountsController):
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_reimbursed_amount(self)
self.set_status() self.set_status(update=True)
self.update_claimed_amount_in_employee_advance() self.update_claimed_amount_in_employee_advance()
def on_cancel(self): def on_cancel(self):
@@ -86,7 +91,6 @@ class ExpenseClaim(AccountsController):
if self.is_paid: if self.is_paid:
update_reimbursed_amount(self) update_reimbursed_amount(self)
self.set_status()
self.update_claimed_amount_in_employee_advance() self.update_claimed_amount_in_employee_advance()
def update_claimed_amount_in_employee_advance(self): def update_claimed_amount_in_employee_advance(self):

View File

@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate
from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver, validate_active_employee
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange from erpnext.buying.doctype.supplier_scorecard.supplier_scorecard import daterange
@@ -22,6 +22,7 @@ class LeaveApplication(Document):
return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type) return _("{0}: From {0} of type {1}").format(self.employee_name, self.leave_type)
def validate(self): def validate(self):
validate_active_employee(self.employee)
set_employee_name(self) set_employee_name(self)
self.validate_dates() self.validate_dates()
self.validate_balance_leaves() self.validate_balance_leaves()

View File

@@ -7,7 +7,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import getdate, nowdate, flt from frappe.utils import getdate, nowdate, flt
from erpnext.hr.utils import set_employee_name from erpnext.hr.utils import set_employee_name, validate_active_employee
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
@@ -15,6 +15,7 @@ from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leav
class LeaveEncashment(Document): class LeaveEncashment(Document):
def validate(self): def validate(self):
set_employee_name(self) set_employee_name(self)
validate_active_employee(self.employee)
self.get_leave_details_for_encashment() self.get_leave_details_for_encashment()
self.validate_salary_structure() self.validate_salary_structure()

View File

@@ -9,10 +9,12 @@ from frappe.model.document import Document
from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, now_datetime, nowdate
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from erpnext.hr.utils import validate_active_employee
from datetime import timedelta, datetime from datetime import timedelta, datetime
class ShiftAssignment(Document): class ShiftAssignment(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_overlapping_dates() self.validate_overlapping_dates()
if self.end_date and self.end_date <= self.start_date: if self.end_date and self.end_date <= self.start_date:

View File

@@ -7,12 +7,13 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import formatdate, getdate from frappe.utils import formatdate, getdate
from erpnext.hr.utils import share_doc_with_approver from erpnext.hr.utils import share_doc_with_approver, validate_active_employee
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class ShiftRequest(Document): class ShiftRequest(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.validate_shift_request_overlap_dates() self.validate_shift_request_overlap_dates()
self.validate_approver() self.validate_approver()

View File

@@ -5,6 +5,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class TravelRequest(Document): class TravelRequest(Document):
pass def validate(self):
validate_active_employee(self.employee)

View File

@@ -3,13 +3,12 @@
import erpnext import erpnext
import frappe import frappe
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee, InactiveEmployeeStatusError
from frappe import _ from frappe import _
from frappe.desk.form import assign_to from frappe.desk.form import assign_to
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate, from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
get_datetime, getdate, nowdate, today, unique) get_datetime, getdate, nowdate, today, unique, get_link_to_form)
class DuplicateDeclarationError(frappe.ValidationError): pass class DuplicateDeclarationError(frappe.ValidationError): pass
@@ -20,6 +19,7 @@ class EmployeeBoardingController(Document):
Assign to the concerned person and roles as per the onboarding/separation template Assign to the concerned person and roles as per the onboarding/separation template
''' '''
def validate(self): def validate(self):
validate_active_employee(self.employee)
# remove the task if linked before submitting the form # remove the task if linked before submitting the form
if self.amended_from: if self.amended_from:
for activity in self.activities: for activity in self.activities:
@@ -522,3 +522,8 @@ def share_doc_with_approver(doc, user):
approver = approvers.get(doc.doctype) approver = approvers.get(doc.doctype)
if doc_before_save.get(approver) != doc.get(approver): if doc_before_save.get(approver) != doc.get(approver):
frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver)) frappe.share.remove(doc.doctype, doc.name, doc_before_save.get(approver))
def validate_active_employee(employee):
if frappe.db.get_value("Employee", employee, "status") == "Inactive":
frappe.throw(_("Transactions cannot be created for an Inactive Employee {0}.").format(
get_link_to_form("Employee", employee)), InactiveEmployeeStatusError)

View File

@@ -83,7 +83,7 @@ frappe.ui.form.on("BOM", {
if (!frm.doc.__islocal && frm.doc.docstatus<2) { if (!frm.doc.__islocal && frm.doc.docstatus<2) {
frm.add_custom_button(__("Update Cost"), function() { frm.add_custom_button(__("Update Cost"), function() {
frm.events.update_cost(frm); frm.events.update_cost(frm, true);
}); });
frm.add_custom_button(__("Browse BOM"), function() { frm.add_custom_button(__("Browse BOM"), function() {
frappe.route_options = { frappe.route_options = {
@@ -318,14 +318,15 @@ frappe.ui.form.on("BOM", {
}) })
}, },
update_cost: function(frm) { update_cost: function(frm, save_doc=false) {
return frappe.call({ return frappe.call({
doc: frm.doc, doc: frm.doc,
method: "update_cost", method: "update_cost",
freeze: true, freeze: true,
args: { args: {
update_parent: true, update_parent: true,
from_child_bom:false save: save_doc,
from_child_bom: false
}, },
callback: function(r) { callback: function(r) {
refresh_field("items"); refresh_field("items");

View File

@@ -330,7 +330,7 @@ class BOM(WebsiteGenerator):
frappe.get_doc("BOM", bom).update_cost(from_child_bom=True) frappe.get_doc("BOM", bom).update_cost(from_child_bom=True)
if not from_child_bom: if not from_child_bom:
frappe.msgprint(_("Cost Updated")) frappe.msgprint(_("Cost Updated"), alert=True)
def update_parent_cost(self): def update_parent_cost(self):
if self.total_cost: if self.total_cost:
@@ -748,7 +748,7 @@ def get_valuation_rate(args):
if valuation_rate <= 0: if valuation_rate <= 0:
last_valuation_rate = frappe.db.sql("""select valuation_rate last_valuation_rate = frappe.db.sql("""select valuation_rate
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where item_code = %s and valuation_rate > 0 where item_code = %s and valuation_rate > 0 and is_cancelled = 0
order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code']) order by posting_date desc, posting_time desc, creation desc limit 1""", args['item_code'])
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0 valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
@@ -1069,13 +1069,6 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if barcodes: if barcodes:
or_cond_filters["name"] = ("in", barcodes) or_cond_filters["name"] = ("in", barcodes)
for cond in get_match_cond(doctype, as_condition=False):
for key, value in cond.items():
if key == doctype:
key = "name"
query_filters[key] = ("in", value)
if filters and filters.get("item_code"): if filters and filters.get("item_code"):
has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants") has_variants = frappe.get_cached_value("Item", filters.get("item_code"), "has_variants")
if not has_variants: if not has_variants:
@@ -1084,7 +1077,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
if filters and filters.get("is_stock_item"): if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1 query_filters["is_stock_item"] = 1
return frappe.get_all("Item", return frappe.get_list("Item",
fields = fields, filters=query_filters, fields = fields, filters=query_filters,
or_filters = or_cond_filters, order_by=order_by, or_filters = or_cond_filters, order_by=order_by,
limit_start=start, limit_page_length=page_len, as_list=1) limit_start=start, limit_page_length=page_len, as_list=1)

View File

@@ -192,11 +192,11 @@ class JobCard(Document):
"completed_qty": args.get("completed_qty") or 0.0 "completed_qty": args.get("completed_qty") or 0.0
}) })
elif args.get("start_time"): elif args.get("start_time"):
new_args = { new_args = frappe._dict({
"from_time": get_datetime(args.get("start_time")), "from_time": get_datetime(args.get("start_time")),
"operation": args.get("sub_operation"), "operation": args.get("sub_operation"),
"completed_qty": 0.0 "completed_qty": 0.0
} })
if employees: if employees:
for name in employees: for name in employees:

View File

@@ -747,9 +747,8 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False):
group by item_code, warehouse group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1) """.format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
def get_warehouse_list(warehouses, warehouse_list=None): def get_warehouse_list(warehouses):
if not warehouse_list: warehouse_list = []
warehouse_list = []
if isinstance(warehouses, str): if isinstance(warehouses, str):
warehouses = json.loads(warehouses) warehouses = json.loads(warehouses)
@@ -761,23 +760,19 @@ def get_warehouse_list(warehouses, warehouse_list=None):
else: else:
warehouse_list.append(row.get("warehouse")) warehouse_list.append(row.get("warehouse"))
return warehouse_list
@frappe.whitelist() @frappe.whitelist()
def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None): def get_items_for_material_requests(doc, warehouses=None, get_parent_warehouse_data=None):
if isinstance(doc, str): if isinstance(doc, str):
doc = frappe._dict(json.loads(doc)) doc = frappe._dict(json.loads(doc))
warehouse_list = []
if warehouses: if warehouses:
get_warehouse_list(warehouses, warehouse_list) warehouses = list(set(get_warehouse_list(warehouses)))
if warehouse_list:
warehouses = list(set(warehouse_list))
if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses: if doc.get("for_warehouse") and not get_parent_warehouse_data and doc.get("for_warehouse") in warehouses:
warehouses.remove(doc.get("for_warehouse")) warehouses.remove(doc.get("for_warehouse"))
warehouse_list = None
doc['mr_items'] = [] doc['mr_items'] = []
po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items') po_items = doc.get('po_items') if doc.get('po_items') else doc.get('items')

View File

@@ -10,7 +10,7 @@ from erpnext.stock.doctype.item.test_item import create_item
from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders from erpnext.manufacturing.doctype.production_plan.production_plan import get_sales_orders
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
class TestProductionPlan(unittest.TestCase): class TestProductionPlan(unittest.TestCase):
def setUp(self): def setUp(self):
@@ -251,6 +251,27 @@ class TestProductionPlan(unittest.TestCase):
pln.cancel() pln.cancel()
frappe.delete_doc("Production Plan", pln.name) frappe.delete_doc("Production Plan", pln.name)
def test_get_warehouse_list_group(self):
"""Check if required warehouses are returned"""
warehouse_json = '[{\"warehouse\":\"_Test Warehouse Group - _TC\"}]'
warehouses = set(get_warehouse_list(warehouse_json))
expected_warehouses = {"_Test Warehouse Group-C1 - _TC", "_Test Warehouse Group-C2 - _TC"}
missing_warehouse = expected_warehouses - warehouses
self.assertTrue(len(missing_warehouse) == 0,
msg=f"Following warehouses were expected {', '.join(missing_warehouse)}")
def test_get_warehouse_list_single(self):
warehouse_json = '[{\"warehouse\":\"_Test Scrap Warehouse - _TC\"}]'
warehouses = set(get_warehouse_list(warehouse_json))
expected_warehouses = {"_Test Scrap Warehouse - _TC", }
self.assertEqual(warehouses, expected_warehouses)
def create_production_plan(**args): def create_production_plan(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -487,21 +487,20 @@ class WorkOrder(Document):
return return
operations = [] operations = []
if not self.use_multi_level_bom:
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity") if self.use_multi_level_bom:
operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
else:
bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation() bom_tree = frappe.get_doc("BOM", self.bom_no).get_tree_representation()
bom_traversal = list(reversed(bom_tree.level_order_traversal())) bom_traversal = reversed(bom_tree.level_order_traversal())
bom_traversal.append(bom_tree) # add operation on top level item last
for d in bom_traversal: for node in bom_traversal:
if d.is_bom: if node.is_bom:
operations.extend(_get_operations(d.name, qty=d.exploded_qty)) operations.extend(_get_operations(node.name, qty=node.exploded_qty))
for correct_index, operation in enumerate(operations, start=1): bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
operation.idx = correct_index operations.extend(_get_operations(self.bom_no, qty=1.0/bom_qty))
for correct_index, operation in enumerate(operations, start=1):
operation.idx = correct_index
self.set('operations', operations) self.set('operations', operations)
self.calculate_time() self.calculate_time()

View File

@@ -293,3 +293,7 @@ erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_export_type_for_gst
erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning

View File

@@ -10,6 +10,7 @@ def execute():
if not frappe.db.has_column('Work Order', 'has_batch_no'): if not frappe.db.has_column('Work Order', 'has_batch_no'):
return return
frappe.reload_doc('manufacturing', 'doctype', 'manufacturing_settings')
if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')): if cint(frappe.db.get_single_value('Manufacturing Settings', 'make_serial_no_batch_from_work_order')):
return return
@@ -29,19 +30,20 @@ def execute():
return return
repost_stock_entries = [] repost_stock_entries = []
stock_entries = frappe.db.sql_list(''' stock_entries = frappe.db.sql_list('''
SELECT SELECT
se.name se.name
FROM FROM
`tabStock Entry` se `tabStock Entry` se
WHERE WHERE
se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders} se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in %s
and not exists( and not exists(
select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1 select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1
) )
Order BY ORDER BY
se.posting_date, se.posting_time se.posting_date, se.posting_time
'''.format(work_orders=tuple(work_orders))) ''', (work_orders,))
if stock_entries: if stock_entries:
print('Length of stock entries', len(stock_entries)) print('Length of stock entries', len(stock_entries))
@@ -107,4 +109,4 @@ def repost_future_sle_and_gle(doc):
"company": doc.company "company": doc.company
}) })
create_repost_item_valuation_entry(args) create_repost_item_valuation_entry(args)

View File

@@ -37,7 +37,7 @@ def execute():
if frappe.db.exists('DocType', 'Opportunity'): if frappe.db.exists('DocType', 'Opportunity'):
opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc') opportunities = frappe.db.get_all('Opportunity', fields=['name', 'mins_to_first_response'], order_by='creation desc')
frappe.reload_doc('crm', 'doctype', 'opportunity') frappe.reload_doctype('Opportunity', force=True)
rename_field('Opportunity', 'mins_to_first_response', 'first_response_time') rename_field('Opportunity', 'mins_to_first_response', 'first_response_time')
# change fieldtype to duration # change fieldtype to duration

View File

@@ -0,0 +1,15 @@
import click
import frappe
def execute():
frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings")
if not frappe.db.get_single_value("Shopify Settings", "enable_shopify"):
return
click.secho(
"Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
"Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations",
fg="yellow",
)

View File

@@ -0,0 +1,24 @@
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
# Update custom fields
fieldname = frappe.db.get_value('Custom Field', {'dt': 'Customer', 'fieldname': 'export_type'})
if fieldname:
frappe.db.set_value('Custom Field', fieldname, 'default', '')
fieldname = frappe.db.get_value('Custom Field', {'dt': 'Supplier', 'fieldname': 'export_type'})
if fieldname:
frappe.db.set_value('Custom Field', fieldname, 'default', '')
# Update Customer/Supplier Masters
frappe.db.sql("""
UPDATE `tabCustomer` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas', 'Deemed Export')
""")
frappe.db.sql("""
UPDATE `tabSupplier` set export_type = '' WHERE gst_category NOT IN ('SEZ', 'Overseas')
""")

View File

@@ -0,0 +1,21 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
def execute():
frappe.reload_doc("setup", "doctype", "Email Digest")
frappe.reload_doc("setup", "doctype", "Email Digest Recipient")
email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list'])
for email_digest in email_digests:
if email_digest.recipient_list:
for recipient in email_digest.recipient_list.split("\n"):
doc = frappe.get_doc({
'doctype': 'Email Digest Recipient',
'parenttype': 'Email Digest',
'parentfield': 'recipients',
'parent': email_digest.name,
'recipient': recipient
})
doc.insert()

View File

@@ -0,0 +1,9 @@
import frappe
def execute():
if frappe.db.has_table("Tax Withholding Category") \
and frappe.db.has_column("Tax Withholding Category", "round_off_tax_amount"):
frappe.db.sql("""
UPDATE `tabTax Withholding Category` set round_off_tax_amount = 0
WHERE round_off_tax_amount IS NULL
""")

View File

@@ -7,6 +7,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _, bold from frappe import _, bold
from frappe.utils import getdate, date_diff, comma_and, formatdate from frappe.utils import getdate, date_diff, comma_and, formatdate
from erpnext.hr.utils import validate_active_employee
class AdditionalSalary(Document): class AdditionalSalary(Document):
def on_submit(self): def on_submit(self):
@@ -19,6 +20,7 @@ class AdditionalSalary(Document):
self.update_employee_referral(cancel=True) self.update_employee_referral(cancel=True)
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.validate_salary_structure() self.validate_salary_structure()
self.validate_recurring_additional_salary_overlap() self.validate_recurring_additional_salary_overlap()

View File

@@ -9,10 +9,11 @@ from frappe.utils import date_diff, getdate, rounded, add_days, cstr, cint, flt
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period_days, get_period_factor
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount from erpnext.hr.utils import get_sal_slip_total_benefit_given, get_holidays_for_employee, get_previous_claimed_amount, validate_active_employee
class EmployeeBenefitApplication(Document): class EmployeeBenefitApplication(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_duplicate_on_payroll_period() self.validate_duplicate_on_payroll_period()
if not self.max_benefits: if not self.max_benefits:
self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period) self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)

View File

@@ -8,12 +8,13 @@ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_max_benefits
from erpnext.hr.utils import get_previous_claimed_amount from erpnext.hr.utils import get_previous_claimed_amount, validate_active_employee
from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period from erpnext.payroll.doctype.payroll_period.payroll_period import get_payroll_period
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_assigned_salary_structure
class EmployeeBenefitClaim(Document): class EmployeeBenefitClaim(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
max_benefits = get_max_benefits(self.employee, self.claim_date) max_benefits = get_max_benefits(self.employee, self.claim_date)
if not max_benefits or max_benefits <= 0: if not max_benefits or max_benefits <= 0:
frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee)) frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee))

View File

@@ -6,9 +6,11 @@ from __future__ import unicode_literals
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from erpnext.hr.utils import validate_active_employee
class EmployeeIncentive(Document): class EmployeeIncentive(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
self.validate_salary_structure() self.validate_salary_structure()
def validate_salary_structure(self): def validate_salary_structure(self):

View File

@@ -8,11 +8,12 @@ from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \
calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period calculate_annual_eligible_hra_exemption, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionDeclaration(Document): class EmployeeTaxExemptionDeclaration(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_tax_declaration(self.declarations) validate_tax_declaration(self.declarations)
validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee)
self.set_total_declared_amount() self.set_total_declared_amount()

View File

@@ -7,11 +7,12 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, \ from erpnext.hr.utils import validate_tax_declaration, get_total_exemption_amount, validate_active_employee, \
calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period calculate_hra_exemption_for_period, validate_duplicate_exemption_for_payroll_period
class EmployeeTaxExemptionProofSubmission(Document): class EmployeeTaxExemptionProofSubmission(Document):
def validate(self): def validate(self):
validate_active_employee(self.employee)
validate_tax_declaration(self.tax_exemption_proofs) validate_tax_declaration(self.tax_exemption_proofs)
self.set_total_actual_amount() self.set_total_actual_amount()
self.set_total_exemption_amount() self.set_total_exemption_amount()

View File

@@ -7,11 +7,10 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe import _ from frappe import _
from frappe.utils import getdate from frappe.utils import getdate
from erpnext.hr.utils import validate_active_employee
class RetentionBonus(Document): class RetentionBonus(Document):
def validate(self): def validate(self):
if frappe.get_value('Employee', self.employee, 'status') != 'Active': validate_active_employee(self.employee)
frappe.throw(_('Cannot create Retention Bonus for Left or Inactive Employees'))
if getdate(self.bonus_payment_date) < getdate(): if getdate(self.bonus_payment_date) < getdate():
frappe.throw(_('Bonus Payment Date cannot be a past date')) frappe.throw(_('Bonus Payment Date cannot be a past date'))

View File

@@ -4,11 +4,18 @@
frappe.ui.form.on('Salary Component', { frappe.ui.form.on('Salary Component', {
setup: function(frm) { setup: function(frm) {
frm.set_query("account", "accounts", function(doc, cdt, cdn) { frm.set_query("account", "accounts", function(doc, cdt, cdn) {
var d = locals[cdt][cdn]; let d = frappe.get_doc(cdt, cdn);
let root_type = "Liability";
if (frm.doc.type == "Deduction") {
root_type = "Expense";
}
return { return {
filters: { filters: {
"is_group": 0, "is_group": 0,
"company": d.company "company": d.company,
"root_type": root_type
} }
}; };
}); });

View File

@@ -19,6 +19,7 @@ from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_appli
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 from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.utils import validate_active_employee
from six import iteritems from six import iteritems
class SalarySlip(TransactionBase): class SalarySlip(TransactionBase):
@@ -39,6 +40,7 @@ class SalarySlip(TransactionBase):
def validate(self): def validate(self):
self.status = self.get_status() self.status = self.get_status()
validate_active_employee(self.employee)
self.validate_dates() self.validate_dates()
self.check_existing() self.check_existing()
if not self.salary_slip_based_on_timesheet: if not self.salary_slip_based_on_timesheet:

View File

@@ -15,12 +15,15 @@ from erpnext.manufacturing.doctype.workstation.workstation import (check_if_with
WorkstationHolidayError) WorkstationHolidayError)
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
from erpnext.hr.utils import validate_active_employee
class OverlapError(frappe.ValidationError): pass class OverlapError(frappe.ValidationError): pass
class OverWorkLoggedError(frappe.ValidationError): pass class OverWorkLoggedError(frappe.ValidationError): pass
class Timesheet(Document): class Timesheet(Document):
def validate(self): def validate(self):
if self.employee:
validate_active_employee(self.employee)
self.set_employee_name() self.set_employee_name()
self.set_status() self.set_status()
self.validate_dates() self.validate_dates()

View File

@@ -0,0 +1,16 @@
frappe.ui.form.on("Contact", {
refresh(frm) {
frm.set_query('link_doctype', "links", function() {
return {
query: "frappe.contacts.address_and_contact.filter_dynamic_link_doctypes",
filters: {
fieldtype: ["in", ["HTML", "Text Editor"]],
fieldname: ["in", ["contact_html", "company_description"]],
}
};
});
frm.refresh_field("links");
}
});

View File

@@ -65,28 +65,23 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
this.frm.refresh_fields(); this.frm.refresh_fields();
}, },
calculate_discount_amount: function(){ calculate_discount_amount: function() {
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) { if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
this.calculate_item_values();
this.calculate_net_total();
this.set_discount_amount(); this.set_discount_amount();
this.apply_discount_amount(); this.apply_discount_amount();
} }
}, },
_calculate_taxes_and_totals: function() { _calculate_taxes_and_totals: function() {
frappe.run_serially([ this.validate_conversion_rate();
() => this.validate_conversion_rate(), this.calculate_item_values();
() => this.calculate_item_values(), this.initialize_taxes();
() => this.update_item_tax_map(), this.determine_exclusive_rate();
() => this.initialize_taxes(), this.calculate_net_total();
() => this.determine_exclusive_rate(), this.calculate_taxes();
() => this.calculate_net_total(), this.manipulate_grand_total_for_inclusive_tax();
() => this.calculate_taxes(), this.calculate_totals();
() => this.manipulate_grand_total_for_inclusive_tax(), this._cleanup();
() => this.calculate_totals(),
() => this._cleanup()
]);
}, },
validate_conversion_rate: function() { validate_conversion_rate: function() {
@@ -107,7 +102,7 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
}, },
calculate_item_values: function() { calculate_item_values: function() {
var me = this; let me = this;
if (!this.discount_amount_applied) { if (!this.discount_amount_applied) {
$.each(this.frm.doc["items"] || [], function(i, item) { $.each(this.frm.doc["items"] || [], function(i, item) {
frappe.model.round_floats_in(item); frappe.model.round_floats_in(item);
@@ -268,46 +263,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
}, },
update_item_tax_map: function() {
let me = this;
let item_codes = [];
let item_rates = {};
let item_tax_templates = {};
$.each(this.frm.doc.items || [], function(i, item) {
if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times
item_codes.push([item.item_code, item.name]);
item_rates[item.name] = item.net_rate;
item_tax_templates[item.name] = item.item_tax_template;
}
});
if (item_codes.length) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_item_tax_info",
args: {
company: me.frm.doc.company,
tax_category: cstr(me.frm.doc.tax_category),
item_codes: item_codes,
item_rates: item_rates,
item_tax_templates: item_tax_templates
},
callback: function(r) {
if (!r.exc) {
$.each(me.frm.doc.items || [], function(i, item) {
if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) {
item.item_tax_template = r.message[item.name].item_tax_template;
item.item_tax_rate = r.message[item.name].item_tax_rate;
me.add_taxes_from_item_tax_template(item.item_tax_rate);
}
});
}
}
});
}
},
add_taxes_from_item_tax_template: function(item_tax_map) { add_taxes_from_item_tax_template: function(item_tax_map) {
let me = this; let me = this;
@@ -632,8 +587,6 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail); tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
}); });
} }
this.frm.refresh_fields();
}, },
set_discount_amount: function() { set_discount_amount: function() {

View File

@@ -826,9 +826,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
frappe.run_serially([ frappe.run_serially([
() => me.frm.script_manager.trigger("currency"), () => me.frm.script_manager.trigger("currency"),
() => me.update_item_tax_map(),
() => me.apply_default_taxes(), () => me.apply_default_taxes(),
() => me.apply_pricing_rule(), () => me.apply_pricing_rule()
() => me.calculate_taxes_and_totals()
]); ]);
} }
} }
@@ -1787,6 +1787,46 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
]); ]);
}, },
update_item_tax_map: function() {
let me = this;
let item_codes = [];
let item_rates = {};
let item_tax_templates = {};
$.each(this.frm.doc.items || [], function(i, item) {
if (item.item_code) {
// Use combination of name and item code in case same item is added multiple times
item_codes.push([item.item_code, item.name]);
item_rates[item.name] = item.net_rate;
item_tax_templates[item.name] = item.item_tax_template;
}
});
if (item_codes.length) {
return this.frm.call({
method: "erpnext.stock.get_item_details.get_item_tax_info",
args: {
company: me.frm.doc.company,
tax_category: cstr(me.frm.doc.tax_category),
item_codes: item_codes,
item_rates: item_rates,
item_tax_templates: item_tax_templates
},
callback: function(r) {
if (!r.exc) {
$.each(me.frm.doc.items || [], function(i, item) {
if (item.name && r.message.hasOwnProperty(item.name) && r.message[item.name].item_tax_template) {
item.item_tax_template = r.message[item.name].item_tax_template;
item.item_tax_rate = r.message[item.name].item_tax_rate;
me.add_taxes_from_item_tax_template(item.item_tax_rate);
}
});
}
}
});
}
},
item_tax_template: function(doc, cdt, cdn) { item_tax_template: function(doc, cdt, cdn) {
var me = this; var me = this;
if(me.frm.updating_party_details) return; if(me.frm.updating_party_details) return;

View File

@@ -3,7 +3,7 @@
frappe.ui.form.on('E Invoice Settings', { frappe.ui.form.on('E Invoice Settings', {
refresh(frm) { refresh(frm) {
const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing'; const docs_link = 'https://docs.erpnext.com/docs/v13/user/manual/en/regional/india/setup-e-invoicing';
frm.dashboard.set_headline( frm.dashboard.set_headline(
__("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`]) __("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`])
); );

View File

@@ -214,9 +214,8 @@ class GSTR3BReport(Document):
for d in item_details: for d in item_details:
if d.item_code not in self.invoice_items.get(d.parent, {}): if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
if i.item_code == d.item_code and i.parent == d.parent))
if d.is_nil_exempt and d.item_code not in self.is_nil_exempt: if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
self.is_nil_exempt.append(d.item_code) self.is_nil_exempt.append(d.item_code)
@@ -281,9 +280,15 @@ class GSTR3BReport(Document):
if self.get('invoice_items'): if self.get('invoice_items'):
# Build itemised tax for export invoices, nil and exempted where tax table is blank # Build itemised tax for export invoices, nil and exempted where tax table is blank
for invoice, items in iteritems(self.invoice_items): for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type') if invoice not in self.items_based_on_tax_rate and self.invoice_detail_map.get(invoice, {}).get('export_type') \
== "Without Payment of Tax"): == "Without Payment of Tax" and self.invoice_detail_map.get(invoice, {}).get('gst_category') == "Overseas":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
else:
for item in items.keys():
if item in self.is_nil_exempt + self.is_non_gst and \
item not in self.items_based_on_tax_rate.get(invoice, {}).get(0, []):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, [])
self.items_based_on_tax_rate[invoice][0].append(item)
def set_outward_taxable_supplies(self): def set_outward_taxable_supplies(self):
inter_state_supply_details = {} inter_state_supply_details = {}
@@ -322,6 +327,9 @@ class GSTR3BReport(Document):
inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value
inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100) inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100)
if self.invoice_cess.get(inv):
self.report_dict['sup_details']['osup_det']['csamt'] += flt(self.invoice_cess.get(inv), 2)
self.set_inter_state_supply(inter_state_supply_details) self.set_inter_state_supply(inter_state_supply_details)
def set_supplies_liable_to_reverse_charge(self): def set_supplies_liable_to_reverse_charge(self):

View File

@@ -966,7 +966,7 @@ class GSPConnector():
"attached_to_doctype": doctype, "attached_to_doctype": doctype,
"attached_to_name": docname, "attached_to_name": docname,
"attached_to_field": "qrcode_image", "attached_to_field": "qrcode_image",
"is_private": 1, "is_private": 0,
"content": qr_image.getvalue()}) "content": qr_image.getvalue()})
_file.save() _file.save()
frappe.db.commit() frappe.db.commit()

View File

@@ -641,7 +641,6 @@ def make_custom_fields(update=True):
'label': 'Export Type', 'label': 'Export Type',
'fieldtype': 'Select', 'fieldtype': 'Select',
'insert_after': 'gst_category', 'insert_after': 'gst_category',
'default': 'Without Payment of Tax',
'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax' 'options': '\nWith Payment of Tax\nWithout Payment of Tax'
} }
@@ -660,7 +659,6 @@ def make_custom_fields(update=True):
'label': 'Export Type', 'label': 'Export Type',
'fieldtype': 'Select', 'fieldtype': 'Select',
'insert_after': 'gst_category', 'insert_after': 'gst_category',
'default': 'Without Payment of Tax',
'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)',
'options': '\nWith Payment of Tax\nWithout Payment of Tax' 'options': '\nWith Payment of Tax\nWithout Payment of Tax'
} }

View File

@@ -431,9 +431,11 @@ def get_ewb_data(dt, dn):
company_address = frappe.get_doc('Address', doc.company_address) company_address = frappe.get_doc('Address', doc.company_address)
billing_address = frappe.get_doc('Address', doc.customer_address) billing_address = frappe.get_doc('Address', doc.customer_address)
#added dispatch address
dispatch_address = frappe.get_doc('Address', doc.dispatch_address_name) if doc.dispatch_address_name else company_address
shipping_address = frappe.get_doc('Address', doc.shipping_address_name) shipping_address = frappe.get_doc('Address', doc.shipping_address_name)
data = get_address_details(data, doc, company_address, billing_address) data = get_address_details(data, doc, company_address, billing_address, dispatch_address)
data.itemList = [] data.itemList = []
data.totalValue = doc.total data.totalValue = doc.total
@@ -519,10 +521,10 @@ def get_gstins_for_company(company):
`tabDynamic Link`.link_name = %(company)s""", {"company": company}) `tabDynamic Link`.link_name = %(company)s""", {"company": company})
return company_gstins return company_gstins
def get_address_details(data, doc, company_address, billing_address): def get_address_details(data, doc, company_address, billing_address, dispatch_address):
data.fromPincode = validate_pincode(company_address.pincode, 'Company Address') data.fromPincode = validate_pincode(company_address.pincode, 'Company Address')
data.fromStateCode = data.actualFromStateCode = validate_state_code( data.fromStateCode = validate_state_code(company_address.gst_state_number, 'Company Address')
company_address.gst_state_number, 'Company Address') data.actualFromStateCode = validate_state_code(dispatch_address.gst_state_number, 'Dispatch Address')
if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15: if not doc.billing_address_gstin or len(doc.billing_address_gstin) < 15:
data.toGstin = 'URP' data.toGstin = 'URP'

View File

@@ -217,9 +217,8 @@ class Gstr1Report(object):
for d in items: for d in items:
if d.item_code not in self.invoice_items.get(d.parent, {}): if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, 0.0)
sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in items self.invoice_items[d.parent][d.item_code] += d.get('taxable_value', 0) or d.get('base_net_amount', 0)
if i.item_code == d.item_code and i.parent == d.parent))
item_tax_rate = {} item_tax_rate = {}
@@ -287,7 +286,8 @@ class Gstr1Report(object):
# Build itemised tax for export invoices where tax table is blank # Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items): for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \ if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": and self.invoices.get(invoice, {}).get('export_type') == "Without Payment of Tax" \
and self.invoices.get(invoice, {}).get('gst_category') == "Overseas":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def get_columns(self): def get_columns(self):

View File

@@ -38,6 +38,8 @@
"col_break46", "col_break46",
"shipping_address_name", "shipping_address_name",
"shipping_address", "shipping_address",
"dispatch_address_name",
"dispatch_address",
"customer_group", "customer_group",
"territory", "territory",
"currency_and_price_list", "currency_and_price_list",
@@ -1486,13 +1488,29 @@
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
},
{
"allow_on_submit": 1,
"fieldname": "dispatch_address_name",
"fieldtype": "Link",
"label": "Dispatch Address Name",
"options": "Address",
"print_hide": 1
},
{
"allow_on_submit": 1,
"depends_on": "dispatch_address_name",
"fieldname": "dispatch_address",
"fieldtype": "Small Text",
"label": "Dispatch Address",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-04-15 23:55:13.439068", "modified": "2021-07-08 21:37:44.177493",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@@ -564,7 +564,6 @@ erpnext.PointOfSale.ItemCart = class {
) )
set_dynamic_rate_header_width(); set_dynamic_rate_header_width();
this.scroll_to_item($item_to_update);
function set_dynamic_rate_header_width() { function set_dynamic_rate_header_width() {
const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount")); const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount"));
@@ -639,12 +638,6 @@ erpnext.PointOfSale.ItemCart = class {
$($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`); $($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`);
} }
scroll_to_item($item) {
if ($item.length === 0) return;
const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop();
this.$cart_items_wrapper.animate({ scrollTop });
}
update_selector_value_in_cart_item(selector, value, item) { update_selector_value_in_cart_item(selector, value, item) {
const $item_to_update = this.get_cart_item(item); const $item_to_update = this.get_cart_item(item);
$item_to_update.attr(`data-${selector}`, escape(value)); $item_to_update.attr(`data-${selector}`, escape(value));

View File

@@ -1,78 +1,31 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
cur_frm.cscript.refresh = function(doc, dt, dn) { frappe.ui.form.on("Email Digest", {
doc = locals[dt][dn]; refresh: function(frm) {
cur_frm.add_custom_button(__('View Now'), function() { if (!frm.is_new()) {
frappe.call({ frm.add_custom_button(__('View Now'), function() {
method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg', frappe.call({
args: { method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg',
name: doc.name args: {
}, name: frm.doc.name
callback: function(r) { },
var d = new frappe.ui.Dialog({ callback: function(r) {
title: __('Email Digest: ') + dn, let d = new frappe.ui.Dialog({
width: 800 title: __('Email Digest: {0}', [frm.doc.name]),
width: 800
});
$(d.body).html(r.message);
d.show();
}
}); });
$(d.body).html(r.message);
d.show();
}
});
}, "fa fa-eye-open", "btn-default");
if (!cur_frm.is_new()) {
cur_frm.add_custom_button(__('Send Now'), function() {
return cur_frm.call('send', null, (r) => {
frappe.show_alert(__('Message Sent'));
}); });
});
frm.add_custom_button(__('Send Now'), function() {
return frm.call('send', null, () => {
frappe.show_alert({ message: __("Message Sent"), indicator: 'green'});
});
});
}
} }
}; });
cur_frm.cscript.addremove_recipients = function(doc, dt, dn) {
// Get user list
return cur_frm.call('get_users', null, function(r) {
// Open a dialog and display checkboxes against email addresses
doc = locals[dt][dn];
var d = new frappe.ui.Dialog({
title: __('Add/Remove Recipients'),
width: 400
});
$.each(r.user_list, function(i, v) {
var fullname = frappe.user.full_name(v.name);
if(fullname !== v.name) fullname = fullname + " &lt;" + v.name + "&gt;";
if(v.enabled==0) {
fullname = repl("<span style='color: red'> %(name)s (" + __("disabled user") + ")</span>", {name: v.name});
}
$('<div class="checkbox"><label>\
<input type="checkbox" data-id="' + v.name + '"'+
(v.checked ? 'checked' : '') +
'> '+ fullname +'</label></div>').appendTo(d.body);
});
// Display add recipients button
d.set_primary_action("Update", function() {
cur_frm.cscript.add_to_rec_list(doc, d.body, r.user_list.length);
});
cur_frm.rec_dialog = d;
d.show();
});
}
cur_frm.cscript.add_to_rec_list = function(doc, dialog, length) {
// add checked users to list of recipients
var rec_list = [];
$(dialog).find('input:checked').each(function(i, input) {
rec_list.push($(input).attr('data-id'));
});
doc.recipient_list = rec_list.join('\n');
cur_frm.rec_dialog.hide();
cur_frm.save();
cur_frm.refresh_fields();
}

File diff suppressed because it is too large Load Diff

View File

@@ -47,19 +47,13 @@ class EmailDigest(Document):
# send email only to enabled users # send email only to enabled users
valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser` valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser`
where enabled=1""")] where enabled=1""")]
recipients = list(filter(lambda r: r in valid_users,
self.recipient_list.split("\n")))
original_user = frappe.session.user if self.recipients:
for row in self.recipients:
if recipients:
for user_id in recipients:
frappe.set_user(user_id)
frappe.set_user_lang(user_id)
msg_for_this_recipient = self.get_msg_html() msg_for_this_recipient = self.get_msg_html()
if msg_for_this_recipient: if msg_for_this_recipient and row.recipient in valid_users:
frappe.sendmail( frappe.sendmail(
recipients=user_id, recipients=row.recipient,
subject=_("{0} Digest").format(self.frequency), subject=_("{0} Digest").format(self.frequency),
message=msg_for_this_recipient, message=msg_for_this_recipient,
reference_doctype = self.doctype, reference_doctype = self.doctype,

View File

@@ -0,0 +1,33 @@
{
"actions": [],
"creation": "2020-06-08 12:19:40.428949",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"recipient"
],
"fields": [
{
"fieldname": "recipient",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Recipient",
"options": "User",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-08-24 23:10:23.217572",
"modified_by": "Administrator",
"module": "Setup",
"name": "Email Digest Recipient",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View 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 EmailDigestRecipient(Document):
pass

View File

@@ -45,9 +45,16 @@ def enable_shopping_cart(args):
def create_email_digest(): def create_email_digest():
from frappe.utils.user import get_system_managers from frappe.utils.user import get_system_managers
system_managers = get_system_managers(only_name=True) system_managers = get_system_managers(only_name=True)
if not system_managers: if not system_managers:
return return
recipients = []
for d in system_managers:
recipients.append({
'recipient': d
})
companies = frappe.db.sql_list("select name FROM `tabCompany`") companies = frappe.db.sql_list("select name FROM `tabCompany`")
for company in companies: for company in companies:
if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company): if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company):
@@ -56,7 +63,7 @@ def create_email_digest():
"name": "Default Weekly Digest - " + company, "name": "Default Weekly Digest - " + company,
"company": company, "company": company,
"frequency": "Weekly", "frequency": "Weekly",
"recipient_list": "\n".join(system_managers) "recipients": recipients
}) })
for df in edigest.meta.get("fields", {"fieldtype": "Check"}): for df in edigest.meta.get("fields", {"fieldtype": "Check"}):
@@ -72,7 +79,7 @@ def create_email_digest():
"name": "Scheduler Errors", "name": "Scheduler Errors",
"company": companies[0], "company": companies[0],
"frequency": "Daily", "frequency": "Daily",
"recipient_list": "\n".join(system_managers), "recipients": recipients,
"scheduler_errors": 1, "scheduler_errors": 1,
"enabled": 1 "enabled": 1
}) })

View File

@@ -162,19 +162,19 @@ def get_batch_qty(batch_no=None, warehouse=None, item_code=None, posting_date=No
out = float(frappe.db.sql("""select sum(actual_qty) out = float(frappe.db.sql("""select sum(actual_qty)
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where warehouse=%s and batch_no=%s {0}""".format(cond), where is_cancelled = 0 and warehouse=%s and batch_no=%s {0}""".format(cond),
(warehouse, batch_no))[0][0] or 0) (warehouse, batch_no))[0][0] or 0)
if batch_no and not warehouse: if batch_no and not warehouse:
out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty out = frappe.db.sql('''select warehouse, sum(actual_qty) as qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where batch_no=%s where is_cancelled = 0 and batch_no=%s
group by warehouse''', batch_no, as_dict=1) group by warehouse''', batch_no, as_dict=1)
if not batch_no and item_code and warehouse: if not batch_no and item_code and warehouse:
out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty out = frappe.db.sql('''select batch_no, sum(actual_qty) as qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where item_code = %s and warehouse=%s where is_cancelled = 0 and item_code = %s and warehouse=%s
group by batch_no''', (item_code, warehouse), as_dict=1) group by batch_no''', (item_code, warehouse), as_dict=1)
return out return out

View File

@@ -32,6 +32,8 @@
"contact_info", "contact_info",
"shipping_address_name", "shipping_address_name",
"shipping_address", "shipping_address",
"dispatch_address_name",
"dispatch_address",
"contact_person", "contact_person",
"contact_display", "contact_display",
"contact_mobile", "contact_mobile",
@@ -1282,13 +1284,28 @@
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Rounded Total" "label": "Disable Rounded Total"
},
{
"fieldname": "dispatch_address_name",
"fieldtype": "Link",
"label": "Dispatch Address Name",
"options": "Address",
"print_hide": 1
},
{
"depends_on": "dispatch_address_name",
"fieldname": "dispatch_address",
"fieldtype": "Small Text",
"label": "Dispatch Address",
"print_hide": 1,
"read_only": 1
} }
], ],
"icon": "fa fa-truck", "icon": "fa fa-truck",
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-06-11 19:27:30.901112", "modified": "2021-07-08 21:37:20.802652",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@@ -239,6 +239,7 @@ def get_available_item_locations_for_batched_item(item_code, from_warehouses, re
and sle.`item_code`=%(item_code)s and sle.`item_code`=%(item_code)s
and sle.`company` = %(company)s and sle.`company` = %(company)s
and batch.disabled = 0 and batch.disabled = 0
and sle.is_cancelled=0
and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s and IFNULL(batch.`expiry_date`, '2200-01-01') > %(today)s
{warehouse_condition} {warehouse_condition}
GROUP BY GROUP BY

View File

@@ -415,7 +415,7 @@ class PurchaseReceipt(BuyingController):
"cost_center": cost_center, "cost_center": cost_center,
"debit": debit, "debit": debit,
"credit": credit, "credit": credit,
"against_account": against_account, "against": against_account,
"remarks": remarks, "remarks": remarks,
} }

View File

@@ -29,13 +29,50 @@ frappe.ui.form.on('Repost Item Valuation', {
}; };
}); });
} }
frm.trigger('setup_realtime_progress');
}, },
setup_realtime_progress: function(frm) {
frappe.realtime.on('item_reposting_progress', data => {
if (frm.doc.name !== data.name) {
return;
}
if (frm.doc.status == 'In Progress') {
frm.doc.current_index = data.current_index;
frm.doc.items_to_be_repost = data.items_to_be_repost;
frm.dashboard.reset();
frm.trigger('show_reposting_progress');
}
});
},
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.status == "Failed" && frm.doc.docstatus==1) { if (frm.doc.status == "Failed" && frm.doc.docstatus==1) {
frm.add_custom_button(__('Restart'), function () { frm.add_custom_button(__('Restart'), function () {
frm.trigger("restart_reposting"); frm.trigger("restart_reposting");
}).addClass("btn-primary"); }).addClass("btn-primary");
} }
frm.trigger('show_reposting_progress');
},
show_reposting_progress: function(frm) {
var bars = [];
let total_count = frm.doc.items_to_be_repost ? JSON.parse(frm.doc.items_to_be_repost).length : 0;
let progress = flt(cint(frm.doc.current_index) / total_count * 100, 2) || 0.5;
var title = __('Reposting Completed {0}%', [progress]);
bars.push({
'title': title,
'width': progress + '%',
'progress_class': 'progress-bar-success'
});
frm.dashboard.add_progress(__('Reposting Progress'), bars);
}, },
restart_reposting: function(frm) { restart_reposting: function(frm) {

View File

@@ -21,7 +21,10 @@
"allow_zero_rate", "allow_zero_rate",
"amended_from", "amended_from",
"error_section", "error_section",
"error_log" "error_log",
"items_to_be_repost",
"distinct_item_and_warehouse",
"current_index"
], ],
"fields": [ "fields": [
{ {
@@ -142,12 +145,39 @@
"fieldname": "allow_zero_rate", "fieldname": "allow_zero_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Zero Rate" "label": "Allow Zero Rate"
},
{
"fieldname": "items_to_be_repost",
"fieldtype": "Code",
"hidden": 1,
"label": "Items to Be Repost",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "distinct_item_and_warehouse",
"fieldtype": "Code",
"hidden": 1,
"label": "Distinct Item and Warehouse",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "current_index",
"fieldtype": "Int",
"hidden": 1,
"label": "Current Index",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-10 07:52:12.476589", "modified": "2021-07-22 18:59:43.057878",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Repost Item Valuation", "name": "Repost Item Valuation",

View File

@@ -80,7 +80,7 @@ def repost(doc):
def repost_sl_entries(doc): def repost_sl_entries(doc):
if doc.based_on == 'Transaction': if doc.based_on == 'Transaction':
repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no, repost_future_sle(doc=doc, voucher_type=doc.voucher_type, voucher_no=doc.voucher_no,
allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher) allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
else: else:
repost_future_sle(args=[frappe._dict({ repost_future_sle(args=[frappe._dict({

View File

@@ -1789,7 +1789,7 @@ def get_expired_batch_items():
from `tabBatch` b, `tabStock Ledger Entry` sle from `tabBatch` b, `tabStock Ledger Entry` sle
where b.expiry_date <= %s where b.expiry_date <= %s
and b.expiry_date is not NULL and b.expiry_date is not NULL
and b.batch_id = sle.batch_no and b.batch_id = sle.batch_no and sle.is_cancelled = 0
group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1) group by sle.warehouse, sle.item_code, sle.batch_no""",(nowdate()), as_dict=1)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -60,7 +60,7 @@ class StockLedgerEntry(Document):
if self.batch_no and not self.get("allow_negative_stock"): if self.batch_no and not self.get("allow_negative_stock"):
batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty) batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where warehouse=%s and item_code=%s and batch_no=%s""", where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""",
(self.warehouse, self.item_code, self.batch_no))[0][0]) (self.warehouse, self.item_code, self.batch_no))[0][0])
if batch_bal_after_transaction < 0: if batch_bal_after_transaction < 0:
@@ -152,7 +152,7 @@ class StockLedgerEntry(Document):
last_transaction_time = frappe.db.sql(""" last_transaction_time = frappe.db.sql("""
select MAX(timestamp(posting_date, posting_time)) as posting_time select MAX(timestamp(posting_date, posting_time)) as posting_time
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where docstatus = 1 and item_code = %s where docstatus = 1 and is_cancelled = 0 and item_code = %s
and warehouse = %s""", (self.item_code, self.warehouse))[0][0] and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00") cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")

View File

@@ -22,6 +22,7 @@ def get_data(report_filters):
data = [] data = []
filters = { filters = {
"is_cancelled": 0,
"company": report_filters.company, "company": report_filters.company,
"posting_date": ("<=", report_filters.as_on_date) "posting_date": ("<=", report_filters.as_on_date)
} }
@@ -34,7 +35,7 @@ def get_data(report_filters):
key = (d.voucher_type, d.voucher_no) key = (d.voucher_type, d.voucher_no)
gl_data = voucher_wise_gl_data.get(key) or {} gl_data = voucher_wise_gl_data.get(key) or {}
d.account_value = gl_data.get("account_value", 0) d.account_value = gl_data.get("account_value", 0)
d.difference_value = (d.stock_value - d.account_value) d.difference_value = abs(d.stock_value - d.account_value)
if abs(d.difference_value) > 0.1: if abs(d.difference_value) > 0.1:
data.append(d) data.append(d)

View File

@@ -16,8 +16,6 @@ def execute(filters=None):
is_reposting_item_valuation_in_progress() is_reposting_item_valuation_in_progress()
if not filters: filters = {} if not filters: filters = {}
validate_filters(filters)
from_date = filters.get('from_date') from_date = filters.get('from_date')
to_date = filters.get('to_date') to_date = filters.get('to_date')
@@ -295,12 +293,6 @@ def get_item_reorder_details(items):
return dict((d.parent + d.warehouse, d) for d in item_reorder_details) return dict((d.parent + d.warehouse, d) for d in item_reorder_details)
def validate_filters(filters):
if not (filters.get("item_code") or filters.get("warehouse")):
sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0])
if sle_count > 500000:
frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries."))
def get_variants_attributes(): def get_variants_attributes():
'''Return all item variant attributes.''' '''Return all item variant attributes.'''
return [i.name for i in frappe.get_all('Item Attribute')] return [i.name for i in frappe.get_all('Item Attribute')]

View File

@@ -69,7 +69,7 @@ def get_consumed_details(filters):
i.stock_uom, sle.actual_qty, sle.stock_value_difference, i.stock_uom, sle.actual_qty, sle.stock_value_difference,
sle.voucher_no, sle.voucher_type sle.voucher_no, sle.voucher_type
from `tabStock Ledger Entry` sle, `tabItem` i from `tabStock Ledger Entry` sle, `tabItem` i
where sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1): where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" % conditions, values, as_dict=1):
consumed_details.setdefault(d.item_code, []).append(d) consumed_details.setdefault(d.item_code, []).append(d)
return consumed_details return consumed_details

View File

@@ -127,30 +127,24 @@ def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
sle.submit() sle.submit()
return sle return sle
def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False): def repost_future_sle(args=None, doc=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False):
if not args and voucher_type and voucher_no: if not args and voucher_type and voucher_no:
args = get_args_for_voucher(voucher_type, voucher_no) args = get_items_to_be_repost(voucher_type, voucher_no, doc)
distinct_item_warehouses = {} distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
for i, d in enumerate(args):
distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({
"reposting_status": False,
"sle": d,
"args_idx": i
}))
i = 0 i = get_current_index(doc) or 0
while i < len(args): while i < len(args):
obj = update_entries_after({ obj = update_entries_after({
"item_code": args[i].item_code, "item_code": args[i].get('item_code'),
"warehouse": args[i].warehouse, "warehouse": args[i].get('warehouse'),
"posting_date": args[i].posting_date, "posting_date": args[i].get('posting_date'),
"posting_time": args[i].posting_time, "posting_time": args[i].get('posting_time'),
"creation": args[i].get("creation"), "creation": args[i].get("creation"),
"distinct_item_warehouses": distinct_item_warehouses "distinct_item_warehouses": distinct_item_warehouses
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher) }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
distinct_item_warehouses[(args[i].item_code, args[i].warehouse)].reposting_status = True distinct_item_warehouses[(args[i].get('item_code'), args[i].get('warehouse'))].reposting_status = True
if obj.new_items_found: if obj.new_items_found:
for item_wh, data in iteritems(distinct_item_warehouses): for item_wh, data in iteritems(distinct_item_warehouses):
@@ -159,11 +153,35 @@ def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negat
args.append(data.sle) args.append(data.sle)
elif data.sle_changed and not data.reposting_status: elif data.sle_changed and not data.reposting_status:
args[data.args_idx] = data.sle args[data.args_idx] = data.sle
data.sle_changed = False data.sle_changed = False
i += 1 i += 1
def get_args_for_voucher(voucher_type, voucher_no): if doc and i % 2 == 0:
update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
if doc and args:
update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses)
def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses):
frappe.db.set_value(doc.doctype, doc.name, {
'items_to_be_repost': json.dumps(args, default=str),
'distinct_item_and_warehouse': json.dumps({str(k): v for k,v in distinct_item_warehouses.items()}, default=str),
'current_index': index
})
frappe.db.commit()
frappe.publish_realtime('item_reposting_progress', {
'name': doc.name,
'items_to_be_repost': json.dumps(args, default=str),
'current_index': index
})
def get_items_to_be_repost(voucher_type, voucher_no, doc=None):
if doc and doc.items_to_be_repost:
return json.loads(doc.items_to_be_repost) or []
return frappe.db.get_all("Stock Ledger Entry", return frappe.db.get_all("Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no}, filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"], fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
@@ -171,6 +189,25 @@ def get_args_for_voucher(voucher_type, voucher_no):
group_by="item_code, warehouse" group_by="item_code, warehouse"
) )
def get_distinct_item_warehouse(args=None, doc=None):
distinct_item_warehouses = {}
if doc and doc.distinct_item_and_warehouse:
distinct_item_warehouses = json.loads(doc.distinct_item_and_warehouse)
distinct_item_warehouses = {frappe.safe_eval(k): frappe._dict(v) for k, v in distinct_item_warehouses.items()}
else:
for i, d in enumerate(args):
distinct_item_warehouses.setdefault((d.item_code, d.warehouse), frappe._dict({
"reposting_status": False,
"sle": d,
"args_idx": i
}))
return distinct_item_warehouses
def get_current_index(doc=None):
if doc and doc.current_index:
return doc.current_index
class update_entries_after(object): class update_entries_after(object):
""" """
update valution rate and qty after transaction update valution rate and qty after transaction