Merge branch 'develop' into maint_sch_link_fix

This commit is contained in:
Noah Jacob 2022-01-17 10:34:07 +05:30 committed by GitHub
commit d39ffeeef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 577 additions and 395 deletions

42
.github/labeler.yml vendored
View File

@ -1,53 +1,53 @@
accounts: accounts:
- 'erpnext/accounts/*' - erpnext/accounts/*
- 'erpnext/controllers/accounts_controller.py' - erpnext/controllers/accounts_controller.py
- 'erpnext/controllers/taxes_and_totals.py' - erpnext/controllers/taxes_and_totals.py
stock: stock:
- 'erpnext/stock/*' - erpnext/stock/*
- 'erpnext/controllers/stock_controller.py' - erpnext/controllers/stock_controller.py
- 'erpnext/controllers/item_variant.py' - erpnext/controllers/item_variant.py
assets: assets:
- 'erpnext/assets/*' - erpnext/assets/*
regional: regional:
- 'erpnext/regional/*' - erpnext/regional/*
selling: selling:
- 'erpnext/selling/*' - erpnext/selling/*
- 'erpnext/controllers/selling_controller.py' - erpnext/controllers/selling_controller.py
buying: buying:
- 'erpnext/buying/*' - erpnext/buying/*
- 'erpnext/controllers/buying_controller.py' - erpnext/controllers/buying_controller.py
support: support:
- 'erpnext/support/*' - erpnext/support/*
POS: POS:
- 'pos*' - pos*
ecommerce: ecommerce:
- 'erpnext/e_commerce/*' - erpnext/e_commerce/*
maintenance: maintenance:
- 'erpnext/maintenance/*' - erpnext/maintenance/*
manufacturing: manufacturing:
- 'erpnext/manufacturing/*' - erpnext/manufacturing/*
crm: crm:
- 'erpnext/crm/*' - erpnext/crm/*
HR: HR:
- 'erpnext/hr/*' - erpnext/hr/*
payroll: payroll:
- 'erpnext/payroll*' - erpnext/payroll*
projects: projects:
- 'erpnext/projects/*' - erpnext/projects/*
# Any python files modifed but no test files modified # Any python files modifed but no test files modified
needs-tests: needs-tests:

View File

@ -254,11 +254,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
enable_check = "enable_deferred_revenue" \ enable_check = "enable_deferred_revenue" \
if doc.doctype=="Sales Invoice" else "enable_deferred_expense" if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
accounts_frozen_upto = frappe.get_cached_value('Accounts Settings', 'None', 'acc_frozen_upto')
def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on): def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date) start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
if not (start_date and end_date): return if not (start_date and end_date): return
account_currency = get_account_currency(item.expense_account) account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice": if doc.doctype == "Sales Invoice":
against, project = doc.customer, doc.project against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account credit_account, debit_account = item.income_account, item.deferred_revenue_account
@ -279,6 +281,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if not amount: if not amount:
return return
# check if books nor frozen till endate:
if getdate(end_date) >= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry: if via_journal_entry:
book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount, book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry) base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
@ -406,8 +412,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': credit_account, 'account': credit_account,
'credit': base_amount, 'credit': base_amount,
'credit_in_account_currency': amount, 'credit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency, 'account_currency': account_currency,
'reference_name': doc.name, 'reference_name': doc.name,
'reference_type': doc.doctype, 'reference_type': doc.doctype,
@ -420,8 +424,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
'account': debit_account, 'account': debit_account,
'debit': base_amount, 'debit': base_amount,
'debit_in_account_currency': amount, 'debit_in_account_currency': amount,
'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
'party': against,
'account_currency': account_currency, 'account_currency': account_currency,
'reference_name': doc.name, 'reference_name': doc.name,
'reference_type': doc.doctype, 'reference_type': doc.doctype,

View File

@ -407,13 +407,14 @@ class JournalEntry(AccountsController):
debit_or_credit = 'Debit' if d.debit else 'Credit' debit_or_credit = 'Debit' if d.debit else 'Credit'
party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no, party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
debit_or_credit) debit_or_credit)
against_voucher = ['', against_voucher[1]]
else: else:
if d.reference_type == "Sales Invoice": if d.reference_type == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
else: else:
party_account = against_voucher[1] party_account = against_voucher[1]
if (against_voucher[0] != d.party or party_account != d.account): if (against_voucher[0] != cstr(d.party) or party_account != d.account):
frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}") frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
.format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1], .format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
d.reference_type, d.reference_name)) d.reference_type, d.reference_name))
@ -478,6 +479,15 @@ class JournalEntry(AccountsController):
def set_against_account(self): def set_against_account(self):
accounts_debited, accounts_credited = [], [] accounts_debited, accounts_credited = [], []
if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
for d in self.get('accounts'):
if d.reference_type == 'Sales Invoice':
field = 'customer'
else:
field = 'supplier'
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else:
for d in self.get("accounts"): for d in self.get("accounts"):
if flt(d.debit > 0): accounts_debited.append(d.party or d.account) if flt(d.debit > 0): accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0: accounts_credited.append(d.party or d.account) if flt(d.credit) > 0: accounts_credited.append(d.party or d.account)

View File

@ -1781,47 +1781,6 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30") check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
frappe.set_user("Administrator")
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
self.assertRaises(frappe.ValidationError, pda1.submit)
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
def test_fixed_deferred_revenue(self): def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue", deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company") parent_account="Current Liabilities - _TC", company="_Test Company")
@ -2482,6 +2441,74 @@ class TestSalesInvoice(unittest.TestCase):
frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance) frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance)
def test_multi_currency_deferred_revenue_via_journal_entry(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 1
acc_settings.submit_journal_entries = 1
acc_settings.save()
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_expense = 1
item.deferred_revenue_account = deferred_account
item.save()
si = create_sales_invoice(customer='_Test Customer USD', currency='USD',
item=item.name, qty=1, rate=100, conversion_rate=60, do_not_save=True)
si.set_posting_time = 1
si.posting_date = '2019-01-01'
si.debit_to = '_Test Receivable USD - _TC'
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-01"
si.items[0].service_end_date = "2019-03-30"
si.items[0].deferred_expense_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
pda1.submit()
expected_gle = [
["Sales - _TC", 0.0, 2089.89, "2019-01-28"],
[deferred_account, 2089.89, 0.0, "2019-01-28"],
["Sales - _TC", 0.0, 1887.64, "2019-02-28"],
[deferred_account, 1887.64, 0.0, "2019-02-28"],
["Sales - _TC", 0.0, 2022.47, "2019-03-15"],
[deferred_account, 2022.47, 0.0, "2019-03-15"]
]
gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s
order by posting_date asc, account asc""", (si.items[0].name, si.posting_date), as_dict=1)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.credit)
self.assertEqual(expected_gle[i][2], gle.debit)
self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
acc_settings = frappe.get_single('Accounts Settings')
acc_settings.book_deferred_entries_via_journal_entry = 0
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def get_sales_invoice_for_e_invoice(): def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill() si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####' si.naming_series = 'INV-2020-.#####'

View File

@ -28,14 +28,14 @@
{ {
"columns": 2, "columns": 2,
"fieldname": "single_threshold", "fieldname": "single_threshold",
"fieldtype": "Currency", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Single Transaction Threshold" "label": "Single Transaction Threshold"
}, },
{ {
"columns": 3, "columns": 3,
"fieldname": "cumulative_threshold", "fieldname": "cumulative_threshold",
"fieldtype": "Currency", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Cumulative Transaction Threshold" "label": "Cumulative Transaction Threshold"
}, },
@ -59,7 +59,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-08-31 11:42:12.213977", "modified": "2022-01-13 12:04:42.904263",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Tax Withholding Rate", "name": "Tax Withholding Rate",
@ -68,5 +68,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -185,8 +185,6 @@ class AccountsController(TransactionBase):
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_end_date): elif getdate(self.posting_date) > getdate(d.service_end_date):
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
elif getdate(self.posting_date) > getdate(d.service_start_date):
frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx))
def validate_invoice_documents_schedule(self): def validate_invoice_documents_schedule(self):
self.validate_payment_schedule_dates() self.validate_payment_schedule_dates()

View File

@ -1,294 +1,108 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "HR-LPR-.YYYY.-.#####", "autoname": "HR-LPR-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 15:20:52.864288", "creation": "2018-04-13 15:20:52.864288",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"from_date",
"to_date",
"is_active",
"column_break_3",
"company",
"optional_holiday_list"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date", "fieldname": "from_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "From Date", "label": "From Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date", "fieldname": "to_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "To Date", "label": "To Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "is_active", "fieldname": "is_active",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Is Active"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Active",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "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,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"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": "Company", "label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company", "options": "Company",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "optional_holiday_list", "fieldname": "optional_holiday_list",
"fieldtype": "Link", "fieldtype": "Link",
"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": "Holiday List for Optional Leave", "label": "Holiday List for Optional Leave",
"length": 0, "options": "Holiday List"
"no_copy": 0,
"options": "Holiday List",
"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
} }
], ],
"has_web_view": 0, "links": [],
"hide_heading": 0, "modified": "2022-01-13 13:28:12.951025",
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2019-05-30 16:15:43.305502",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Period", "name": "Leave Period",
"name_case": "", "naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR User", "role": "HR User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0, "search_fields": "from_date, to_date, company",
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "states": [],
"track_seen": 0, "track_changes": 1
"track_views": 0
} }

View File

@ -113,10 +113,11 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-01 17:54:01.014509", "modified": "2022-01-13 13:37:11.218882",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Policy Assignment", "name": "Leave Policy Assignment",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -164,5 +165,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "employee_name",
"track_changes": 1 "track_changes": 1
} }

View File

@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = {
if (cur_dialog.fields_dict.leave_period.value) { if (cur_dialog.fields_dict.leave_period.value) {
me.set_effective_date(); me.set_effective_date();
} }
} },
get_query() {
let filters = {"is_active": 1};
if (cur_dialog.fields_dict.company.value)
filters["company"] = cur_dialog.fields_dict.company.value;
return {
filters: filters
};
},
}, },
{ {
fieldtype: "Column Break" fieldtype: "Column Break"

View File

@ -279,7 +279,7 @@ erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.update_recipient_email_digest erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.remove_bad_selling_defaults erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.trim_whitespace_from_serial_nos erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022
erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.einvoicing_deprecation_warning erpnext.patches.v13_0.einvoicing_deprecation_warning
@ -313,8 +313,8 @@ erpnext.patches.v14_0.delete_healthcare_doctypes
erpnext.patches.v13_0.update_category_in_ltds_certificate erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
erpnext.patches.v13_0.create_ksa_vat_custom_fields # 07-01-2022
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.migrate_crm_settings erpnext.patches.v14_0.migrate_crm_settings
erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.rename_ksa_qr_field
@ -326,3 +326,4 @@ erpnext.patches.v14_0.set_payroll_cost_centers
erpnext.patches.v13_0.agriculture_deprecation_warning erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v14_0.delete_agriculture_doctypes erpnext.patches.v14_0.delete_agriculture_doctypes
erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v14_0.rearrange_company_fields

View File

@ -9,13 +9,15 @@ def execute():
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where where
is_cancelled = 0 is_cancelled = 0
and (serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s) and ( serial_no like %s or serial_no like %s or serial_no like %s or serial_no like %s
or serial_no = %s )
""", """,
( (
" %", # leading whitespace " %", # leading whitespace
"% ", # trailing whitespace "% ", # trailing whitespace
"%\n %", # leading whitespace on newline "%\n %", # leading whitespace on newline
"% \n%", # trailing whitespace on newline "% \n%", # trailing whitespace on newline
"\n", # just new line
), ),
as_dict=True, as_dict=True,
) )

View File

@ -47,3 +47,18 @@ def execute():
frappe.delete_doc("DocType", doctype, ignore_missing=True) frappe.delete_doc("DocType", doctype, ignore_missing=True)
frappe.delete_doc("Module Def", "Healthcare", ignore_missing=True, force=True) frappe.delete_doc("Module Def", "Healthcare", ignore_missing=True, force=True)
custom_fields = {
'Sales Invoice': ['patient', 'patient_name', 'ref_practitioner'],
'Sales Invoice Item': ['reference_dt', 'reference_dn'],
'Stock Entry': ['inpatient_medication_entry'],
'Stock Entry Detail': ['patient', 'inpatient_medication_entry_child'],
}
for doc, fields in custom_fields.items():
filters = {
'dt': doc,
'fieldname': ['in', fields]
}
records = frappe.get_all('Custom Field', filters=filters, pluck='name')
for record in records:
frappe.delete_doc('Custom Field', record, ignore_missing=True, force=True)

View File

@ -0,0 +1,31 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
frappe.reload_doc('setup', 'doctype', 'company')
custom_fields = {
'Company': [
dict(fieldname='hra_section', label='HRA Settings',
fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
dict(fieldname='basic_component', label='Basic Component',
fieldtype='Link', options='Salary Component', insert_after='hra_section'),
dict(fieldname='hra_component', label='HRA Component',
fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'),
dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_column_break'),
dict(fieldname='non_profit_section', label='Non Profit Settings',
fieldtype='Section Break', insert_after='arrear_component', collapsible=1),
dict(fieldname='company_80g_number', label='80G Number',
fieldtype='Data', insert_after='non_profit_section'),
dict(fieldname='with_effect_from', label='80G With Effect From',
fieldtype='Date', insert_after='company_80g_number'),
dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'),
dict(fieldname='pan_details', label='PAN Number',
fieldtype='Data', insert_after='non_profit_column_break')
]
}
create_custom_fields(custom_fields, update=True)

View File

@ -61,6 +61,8 @@ class PayrollEntry(Document):
def on_cancel(self): def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
where payroll_entry=%s """, (self.name))) where payroll_entry=%s """, (self.name)))
self.db_set("salary_slips_created", 0)
self.db_set("salary_slips_submitted", 0)
def get_emp_list(self): def get_emp_list(self):
""" """

View File

@ -567,16 +567,16 @@ def get_custom_fields():
fieldtype='Link', options='Salary Component', insert_after='basic_component'), fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'), dict(fieldname='hra_column_break', fieldtype='Column Break', insert_after='hra_component'),
dict(fieldname='arrear_component', label='Arrear Component', dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_component'), fieldtype='Link', options='Salary Component', insert_after='hra_column_break'),
dict(fieldname='non_profit_section', label='Non Profit Settings', dict(fieldname='non_profit_section', label='Non Profit Settings',
fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1), fieldtype='Section Break', insert_after='arrear_component', collapsible=1),
dict(fieldname='company_80g_number', label='80G Number', dict(fieldname='company_80g_number', label='80G Number',
fieldtype='Data', insert_after='non_profit_section'), fieldtype='Data', insert_after='non_profit_section'),
dict(fieldname='with_effect_from', label='80G With Effect From', dict(fieldname='with_effect_from', label='80G With Effect From',
fieldtype='Date', insert_after='company_80g_number'), fieldtype='Date', insert_after='company_80g_number'),
dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'), dict(fieldname='non_profit_column_break', fieldtype='Column Break', insert_after='with_effect_from'),
dict(fieldname='pan_details', label='PAN Number', dict(fieldname='pan_details', label='PAN Number',
fieldtype='Data', insert_after='with_effect_from') fieldtype='Data', insert_after='non_profit_column_break')
], ],
'Employee Tax Exemption Declaration':[ 'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption', dict(fieldname='hra_section', label='HRA Exemption',

View File

@ -53,7 +53,8 @@ frappe.query_reports["GSTR-1"] = {
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") }, { "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
{ "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") }, { "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") },
{ "value": "EXPORT", "label": __("Export Invoice - 6A") }, { "value": "EXPORT", "label": __("Export Invoice - 6A") },
{ "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") } { "value": "Advances", "label": __("Tax Liability (Advances Received) - 11A(1), 11A(2)") },
{ "value": "NIL Rated", "label": __("NIL RATED/EXEMPTED Invoices") }
], ],
"default": "B2B" "default": "B2B"
} }

View File

@ -40,7 +40,8 @@ class Gstr1Report(object):
port_code, port_code,
shipping_bill_number, shipping_bill_number,
shipping_bill_date, shipping_bill_date,
reason_for_issuing_document reason_for_issuing_document,
company_gstin
""" """
def run(self): def run(self):
@ -62,6 +63,8 @@ class Gstr1Report(object):
self.get_b2c_data() self.get_b2c_data()
elif self.filters.get("type_of_business") == "Advances": elif self.filters.get("type_of_business") == "Advances":
self.get_advance_data() self.get_advance_data()
elif self.filters.get("type_of_business") == "NIL Rated":
self.get_nil_rated_invoices()
elif self.invoices: elif self.invoices:
for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv) invoice_details = self.invoices.get(inv)
@ -91,6 +94,57 @@ class Gstr1Report(object):
row= [key[0], key[1], value[0], value[1]] row= [key[0], key[1], value[0], value[1]]
self.data.append(row) self.data.append(row)
def get_nil_rated_invoices(self):
nil_exempt_output = [
{
"description": "Inter-State supplies to registered persons",
"nil_rated": 0.0,
"exempted": 0.0,
"non_gst": 0.0
},
{
"description": "Intra-State supplies to registered persons",
"nil_rated": 0.0,
"exempted": 0.0,
"non_gst": 0.0
},
{
"description": "Inter-State supplies to unregistered persons",
"nil_rated": 0.0,
"exempted": 0.0,
"non_gst": 0.0
},
{
"description": "Intra-State supplies to unregistered persons",
"nil_rated": 0.0,
"exempted": 0.0,
"non_gst": 0.0
}
]
for invoice, details in self.nil_exempt_non_gst.items():
invoice_detail = self.invoices.get(invoice)
if invoice_detail.get('gst_category') in ("Registered Regular", "Deemed Export", "SEZ"):
if is_inter_state(invoice_detail):
nil_exempt_output[0]["nil_rated"] += details[0]
nil_exempt_output[0]["exempted"] += details[1]
nil_exempt_output[0]["non_gst"] += details[2]
else:
nil_exempt_output[1]["nil_rated"] += details[0]
nil_exempt_output[1]["exempted"] += details[1]
nil_exempt_output[1]["non_gst"] += details[2]
else:
if is_inter_state(invoice_detail):
nil_exempt_output[2]["nil_rated"] += details[0]
nil_exempt_output[2]["exempted"] += details[1]
nil_exempt_output[2]["non_gst"] += details[2]
else:
nil_exempt_output[3]["nil_rated"] += details[0]
nil_exempt_output[3]["exempted"] += details[1]
nil_exempt_output[3]["non_gst"] += details[2]
self.data = nil_exempt_output
def get_b2c_data(self): def get_b2c_data(self):
b2cs_output = {} b2cs_output = {}
@ -240,10 +294,11 @@ class Gstr1Report(object):
def get_invoice_items(self): def get_invoice_items(self):
self.invoice_items = frappe._dict() self.invoice_items = frappe._dict()
self.item_tax_rate = frappe._dict() self.item_tax_rate = frappe._dict()
self.nil_exempt_non_gst = {}
items = frappe.db.sql(""" items = frappe.db.sql("""
select item_code, parent, taxable_value, base_net_amount, item_tax_rate select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt,
from `tab%s Item` is_non_gst from `tab%s Item`
where parent in (%s) where parent in (%s)
""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
@ -260,6 +315,16 @@ class Gstr1Report(object):
tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, []) tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, [])
tax_rate_dict.append(rate) tax_rate_dict.append(rate)
if d.is_nil_exempt:
self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
if item_tax_rate:
self.nil_exempt_non_gst[d.parent][0] += d.get('taxable_value', 0)
else:
self.nil_exempt_non_gst[d.parent][1] += d.get('taxable_value', 0)
elif d.is_non_gst:
self.nil_exempt_non_gst.setdefault(d.parent, [0.0, 0.0, 0.0])
self.nil_exempt_non_gst[d.parent][2] += d.get('taxable_value', 0)
def get_items_based_on_tax_rate(self): def get_items_based_on_tax_rate(self):
self.tax_details = frappe.db.sql(""" self.tax_details = frappe.db.sql("""
select select
@ -322,6 +387,10 @@ class Gstr1Report(object):
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):
self.other_columns = []
self.tax_columns = []
if self.filters.get("type_of_business") != "NIL Rated":
self.tax_columns = [ self.tax_columns = [
{ {
"fieldname": "rate", "fieldname": "rate",
@ -336,7 +405,6 @@ class Gstr1Report(object):
"width": 100 "width": 100
} }
] ]
self.other_columns = []
if self.filters.get("type_of_business") == "B2B": if self.filters.get("type_of_business") == "B2B":
self.invoice_columns = [ self.invoice_columns = [
@ -705,6 +773,33 @@ class Gstr1Report(object):
"width": 100 "width": 100
} }
] ]
elif self.filters.get("type_of_business") == "NIL Rated":
self.invoice_columns = [
{
"fieldname": "description",
"label": "Description",
"fieldtype": "Data",
"width": 420
},
{
"fieldname": "nil_rated",
"label": "Nil Rated",
"fieldtype": "Currency",
"width": 200
},
{
"fieldname": "exempted",
"label": "Exempted",
"fieldtype": "Currency",
"width": 200
},
{
"fieldname": "non_gst",
"label": "Non GST",
"fieldtype": "Currency",
"width": 200
}
]
self.columns = self.invoice_columns + self.tax_columns + self.other_columns self.columns = self.invoice_columns + self.tax_columns + self.other_columns
@ -768,6 +863,11 @@ def get_json(filters, report_name, data):
out = get_advances_json(res, gstin) out = get_advances_json(res, gstin)
gst_json["at"] = out gst_json["at"] = out
elif filters["type_of_business"] == "NIL Rated":
res = report_data[:-1]
out = get_exempted_json(res)
gst_json["nil"] = out
return { return {
'report_name': report_name, 'report_name': report_name,
'report_type': filters['type_of_business'], 'report_type': filters['type_of_business'],
@ -980,6 +1080,36 @@ def get_cdnr_unreg_json(res, gstin):
return out return out
def get_exempted_json(data):
out = {
"inv": [
{
"sply_ty": "INTRB2B"
},
{
"sply_ty": "INTRAB2B"
},
{
"sply_ty": "INTRB2C"
},
{
"sply_ty": "INTRAB2C"
}
]
}
for i, v in enumerate(data):
if data[i].get('nil_rated'):
out['inv'][i]['nil_amt'] = data[i]['nil_rated']
if data[i].get('exempted'):
out['inv'][i]['expt_amt'] = data[i]['exempted']
if data[i].get('non_gst'):
out['inv'][i]['ngsup_amt'] = data[i]['non_gst']
return out
def get_invoice_type_for_cdnr(row): def get_invoice_type_for_cdnr(row):
if row.get('gst_category') == 'SEZ': if row.get('gst_category') == 'SEZ':
if row.get('export_type') == 'WPAY': if row.get('export_type') == 'WPAY':
@ -1064,3 +1194,9 @@ def download_json_file():
frappe.response['filecontent'] = data['data'] frappe.response['filecontent'] = data['data']
frappe.response['content_type'] = 'application/json' frappe.response['content_type'] = 'application/json'
frappe.response['type'] = 'download' frappe.response['type'] = 'download'
def is_inter_state(invoice_detail):
if invoice_detail.place_of_supply.split("-")[0] != invoice_detail.company_gstin[:2]:
return True
else:
return False

View File

@ -3,12 +3,10 @@
import frappe import frappe
from frappe.permissions import add_permission, update_permission_property from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def setup(company=None, patch=True): def setup(company=None, patch=True):
uae_custom_fields()
add_print_formats() add_print_formats()
add_permissions() add_permissions()
make_custom_fields() make_custom_fields()
@ -40,38 +38,67 @@ def make_custom_fields():
- Company Name in Arabic - Company Name in Arabic
- Address in Arabic - Address in Arabic
""" """
is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated',
fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description',
print_hide=1)
is_exempt = dict(fieldname='is_exempt', label='Is Exempt',
fieldtype='Check', fetch_from='item_code.is_exempt', insert_after='is_zero_rated',
print_hide=1)
purchase_invoice_fields = [
dict(fieldname='company_trn', label='Company TRN',
fieldtype='Read Only', insert_after='shipping_address',
fetch_from='company.tax_id', print_hide=1),
dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic',
fieldtype='Read Only', insert_after='supplier_name',
fetch_from='supplier.supplier_name_in_arabic', print_hide=1)
]
sales_invoice_fields = [
dict(fieldname='company_trn', label='Company TRN',
fieldtype='Read Only', insert_after='company_address',
fetch_from='company.tax_id', print_hide=1),
dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic',
fieldtype='Read Only', insert_after='customer_name',
fetch_from='customer.customer_name_in_arabic', print_hide=1),
dict(fieldname='ksa_einv_qr', label='KSA E-Invoicing QR',
fieldtype='Attach Image', read_only=1, no_copy=1, hidden=1)
]
custom_fields = { custom_fields = {
'Sales Invoice': [ 'Item': [is_zero_rated, is_exempt],
dict( 'Customer': [
fieldname='ksa_einv_qr', dict(fieldname='customer_name_in_arabic', label='Customer Name in Arabic',
label='KSA E-Invoicing QR', fieldtype='Data', insert_after='customer_name'),
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
], ],
'POS Invoice': [ 'Supplier': [
dict( dict(fieldname='supplier_name_in_arabic', label='Supplier Name in Arabic',
fieldname='ksa_einv_qr', fieldtype='Data', insert_after='supplier_name'),
label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
], ],
'Purchase Invoice': purchase_invoice_fields,
'Purchase Order': purchase_invoice_fields,
'Purchase Receipt': purchase_invoice_fields,
'Sales Invoice': sales_invoice_fields,
'POS Invoice': sales_invoice_fields,
'Sales Order': sales_invoice_fields,
'Delivery Note': sales_invoice_fields,
'Sales Invoice Item': [is_zero_rated, is_exempt],
'POS Invoice Item': [is_zero_rated, is_exempt],
'Purchase Invoice Item': [is_zero_rated, is_exempt],
'Sales Order Item': [is_zero_rated, is_exempt],
'Delivery Note Item': [is_zero_rated, is_exempt],
'Quotation Item': [is_zero_rated, is_exempt],
'Purchase Order Item': [is_zero_rated, is_exempt],
'Purchase Receipt Item': [is_zero_rated, is_exempt],
'Supplier Quotation Item': [is_zero_rated, is_exempt],
'Address': [ 'Address': [
dict( dict(fieldname='address_in_arabic', label='Address in Arabic',
fieldname='address_in_arabic', fieldtype='Data',insert_after='address_line2')
label='Address in Arabic',
fieldtype='Data',
insert_after='address_line2'
)
], ],
'Company': [ 'Company': [
dict( dict(fieldname='company_name_in_arabic', label='Company Name In Arabic',
fieldname='company_name_in_arabic', fieldtype='Data', insert_after='company_name')
label='Company Name In Arabic',
fieldtype='Data',
insert_after='company_name'
)
] ]
} }

View File

@ -213,6 +213,9 @@ erpnext.company.setup_queries = function(frm) {
["default_payroll_payable_account", {"root_type": "Liability"}], ["default_payroll_payable_account", {"root_type": "Liability"}],
["round_off_account", {"root_type": "Expense"}], ["round_off_account", {"root_type": "Expense"}],
["write_off_account", {"root_type": "Expense"}], ["write_off_account", {"root_type": "Expense"}],
["default_deferred_expense_account", {}],
["default_deferred_revenue_account", {}],
["default_expense_claim_payable_account", {}],
["default_discount_account", {}], ["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}], ["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}], ["discount_received_account", {"root_type": "Income"}],

View File

@ -292,6 +292,7 @@ def get_batches(item_code, warehouse, qty=1, throw=False, serial_no=None):
join `tabStock Ledger Entry` ignore index (item_code, warehouse) join `tabStock Ledger Entry` ignore index (item_code, warehouse)
on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no ) on (`tabBatch`.batch_id = `tabStock Ledger Entry`.batch_no )
where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s where `tabStock Ledger Entry`.item_code = %s and `tabStock Ledger Entry`.warehouse = %s
and `tabStock Ledger Entry`.is_cancelled = 0
and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0} and (`tabBatch`.expiry_date >= CURDATE() or `tabBatch`.expiry_date IS NULL) {0}
group by batch_id group by batch_id
order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC order by `tabBatch`.expiry_date ASC, `tabBatch`.creation ASC

View File

@ -402,10 +402,16 @@ def update_serial_nos(sle, item_det):
def get_auto_serial_nos(serial_no_series, qty): def get_auto_serial_nos(serial_no_series, qty):
serial_nos = [] serial_nos = []
for i in range(cint(qty)): for i in range(cint(qty)):
serial_nos.append(make_autoname(serial_no_series, "Serial No")) serial_nos.append(get_new_serial_number(serial_no_series))
return "\n".join(serial_nos) return "\n".join(serial_nos)
def get_new_serial_number(series):
sr_no = make_autoname(series, "Serial No")
if frappe.db.exists("Serial No", sr_no):
sr_no = get_new_serial_number(series)
return sr_no
def auto_make_serial_nos(args): def auto_make_serial_nos(args):
serial_nos = get_serial_nos(args.get('serial_no')) serial_nos = get_serial_nos(args.get('serial_no'))
created_numbers = [] created_numbers = []

View File

@ -8,6 +8,7 @@
import frappe import frappe
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
@ -176,6 +177,24 @@ class TestSerialNo(ERPNextTestCase):
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC") self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
self.assertEqual(sn_doc.purchase_document_no, se.name) self.assertEqual(sn_doc.purchase_document_no, se.name)
def test_auto_creation_of_serial_no(self):
"""
Test if auto created Serial No excludes existing serial numbers
"""
item_code = make_item("_Test Auto Serial Item ", {
"has_serial_no": 1,
"serial_no_series": "XYZ.###"
}).item_code
# Reserve XYZ005
pr_1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no="XYZ005")
# XYZ005 is already used and will throw an error if used again
pr_2 = make_purchase_receipt(item_code=item_code, qty=10)
self.assertEqual(get_serial_nos(pr_1.get("items")[0].serial_no)[0], "XYZ005")
for serial_no in get_serial_nos(pr_2.get("items")[0].serial_no):
self.assertNotEqual(serial_no, "XYZ005")
def test_serial_no_sanitation(self): def test_serial_no_sanitation(self):
"Test if Serial No input is sanitised before entering the DB." "Test if Serial No input is sanitised before entering the DB."
item_code = "_Test Serialized Item" item_code = "_Test Serialized Item"

View File

@ -86,8 +86,11 @@ class StockEntry(StockController):
self.validate_warehouse() self.validate_warehouse()
self.validate_work_order() self.validate_work_order()
self.validate_bom() self.validate_bom()
if self.purpose in ("Manufacture", "Repack"):
self.mark_finished_and_scrap_items() self.mark_finished_and_scrap_items()
self.validate_finished_goods() self.validate_finished_goods()
self.validate_with_material_request() self.validate_with_material_request()
self.validate_batch() self.validate_batch()
self.validate_inspection() self.validate_inspection()
@ -706,7 +709,6 @@ class StockEntry(StockController):
validate_bom_no(item_code, d.bom_no) validate_bom_no(item_code, d.bom_no)
def mark_finished_and_scrap_items(self): def mark_finished_and_scrap_items(self):
if self.purpose in ("Repack", "Manufacture"):
if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]): if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
return return
@ -738,9 +740,9 @@ class StockEntry(StockController):
def validate_finished_goods(self): def validate_finished_goods(self):
""" """
1. Check if FG exists 1. Check if FG exists (mfg, repack)
2. Check if Multiple FG Items are present 2. Check if Multiple FG Items are present (mfg)
3. Check FG Item and Qty against WO if present 3. Check FG Item and Qty against WO if present (mfg)
""" """
production_item, wo_qty, finished_items = None, 0, [] production_item, wo_qty, finished_items = None, 0, []
@ -753,8 +755,9 @@ class StockEntry(StockController):
for d in self.get('items'): for d in self.get('items'):
if d.is_finished_item: if d.is_finished_item:
if not self.work_order: if not self.work_order:
# Independent MFG Entry/ Repack Entry, no WO to match against
finished_items.append(d.item_code) finished_items.append(d.item_code)
continue # Independent Manufacture Entry, no WO to match against continue
if d.item_code != production_item: if d.item_code != production_item:
frappe.throw(_("Finished Item {0} does not match with Work Order {1}") frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
@ -767,19 +770,17 @@ class StockEntry(StockController):
finished_items.append(d.item_code) finished_items.append(d.item_code)
if len(set(finished_items)) > 1:
frappe.throw(
msg=_("Multiple items cannot be marked as finished item"),
title=_("Note"),
exc=FinishedGoodError
)
if self.purpose == "Manufacture":
if not finished_items: if not finished_items:
frappe.throw( frappe.throw(
msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name), msg=_("There must be atleast 1 Finished Good in this Stock Entry").format(self.name),
title=_("Missing Finished Good"), title=_("Missing Finished Good"), exc=FinishedGoodError
exc=FinishedGoodError )
if self.purpose == "Manufacture":
if len(set(finished_items)) > 1:
frappe.throw(
msg=_("Multiple items cannot be marked as finished item"),
title=_("Note"), exc=FinishedGoodError
) )
allowance_percentage = flt( allowance_percentage = flt(

View File

@ -226,9 +226,47 @@ class TestStockEntry(ERPNextTestCase):
mtn.cancel() mtn.cancel()
def test_repack_no_change_in_valuation(self): def test_repack_multiple_fg(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company') "Test `is_finished_item` for one item repacked into two items."
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
repack = frappe.copy_doc(test_records[3])
repack.posting_date = nowdate()
repack.posting_time = nowtime()
repack.items[0].qty = 100.0
repack.items[0].transfer_qty = 100.0
repack.items[1].qty = 50.0
repack.append("items", {
"conversion_factor": 1.0,
"cost_center": "_Test Cost Center - _TC",
"doctype": "Stock Entry Detail",
"expense_account": "Stock Adjustment - _TC",
"basic_rate": 150,
"item_code": "_Test Item 2",
"parentfield": "items",
"qty": 50.0,
"stock_uom": "_Test UOM",
"t_warehouse": "_Test Warehouse - _TC",
"transfer_qty": 50.0,
"uom": "_Test UOM"
})
repack.set_stock_entry_type()
repack.insert()
self.assertEqual(repack.items[1].is_finished_item, 1)
self.assertEqual(repack.items[2].is_finished_item, 1)
repack.items[1].is_finished_item = 0
repack.items[2].is_finished_item = 0
# must raise error if 0 fg in repack entry
self.assertRaises(FinishedGoodError, repack.validate_finished_goods)
repack.delete() # teardown
def test_repack_no_change_in_valuation(self):
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100) make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
qty=50, basic_rate=100) qty=50, basic_rate=100)

View File

@ -55,7 +55,8 @@ def get_stock_ledger_entries(filters):
return frappe.db.sql("""select item_code, batch_no, warehouse, return frappe.db.sql("""select item_code, batch_no, warehouse,
posting_date, actual_qty posting_date, actual_qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry`
where docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" % where is_cancelled = 0
and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" %
conditions, as_dict=1) conditions, as_dict=1)
def get_item_warehouse_batch_map(filters, float_precision): def get_item_warehouse_batch_map(filters, float_precision):

View File

@ -91,7 +91,7 @@ def get_stock_value_difference_list(filtered_entries: FilteredEntries) -> SVDLis
voucher_nos = [fe.get('voucher_no') for fe in filtered_entries] voucher_nos = [fe.get('voucher_no') for fe in filtered_entries]
svd_list = frappe.get_list( svd_list = frappe.get_list(
'Stock Ledger Entry', fields=['item_code','stock_value_difference'], 'Stock Ledger Entry', fields=['item_code','stock_value_difference'],
filters=[('voucher_no', 'in', voucher_nos)] filters=[('voucher_no', 'in', voucher_nos), ("is_cancelled", "=", 0)]
) )
assign_item_groups_to_svd_list(svd_list) assign_item_groups_to_svd_list(svd_list)
return svd_list return svd_list

View File

@ -76,6 +76,7 @@ def get_consumed_items(condition):
on sle.voucher_no = se.name on sle.voucher_no = se.name
where where
actual_qty < 0 actual_qty < 0
and is_cancelled = 0
and voucher_type not in ('Delivery Note', 'Sales Invoice') and voucher_type not in ('Delivery Note', 'Sales Invoice')
%s %s
group by item_code""" % condition, as_dict=1) group by item_code""" % condition, as_dict=1)

View File

@ -105,6 +105,7 @@ def get_args_for_future_sle(row):
def validate_serial_no(sle): def validate_serial_no(sle):
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 sn in get_serial_nos(sle.serial_no): for sn in get_serial_nos(sle.serial_no):
args = copy.deepcopy(sle) args = copy.deepcopy(sle)
args.serial_no = sn args.serial_no = sn
@ -423,6 +424,8 @@ class update_entries_after(object):
return sorted(entries_to_fix, key=lambda k: k['timestamp']) return sorted(entries_to_fix, key=lambda k: k['timestamp'])
def process_sle(self, sle): def process_sle(self, sle):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
# previous sle data for this warehouse # previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse] self.wh_data = self.data[sle.warehouse]
@ -437,7 +440,7 @@ class update_entries_after(object):
if not self.args.get("sle_id"): if not self.args.get("sle_id"):
self.get_dynamic_incoming_outgoing_rate(sle) self.get_dynamic_incoming_outgoing_rate(sle)
if sle.serial_no: if get_serial_nos(sle.serial_no):
self.get_serialized_values(sle) self.get_serialized_values(sle)
self.wh_data.qty_after_transaction += flt(sle.actual_qty) self.wh_data.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation": if sle.voucher_type == "Stock Reconciliation":
@ -449,8 +452,9 @@ class update_entries_after(object):
# assert # assert
self.wh_data.valuation_rate = sle.valuation_rate self.wh_data.valuation_rate = sle.valuation_rate
self.wh_data.qty_after_transaction = sle.qty_after_transaction self.wh_data.qty_after_transaction = sle.qty_after_transaction
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate) self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
if self.valuation_method != "Moving Average":
self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
else: else:
if self.valuation_method == "Moving Average": if self.valuation_method == "Moving Average":
self.get_moving_average_values(sle) self.get_moving_average_values(sle)
@ -646,6 +650,7 @@ class update_entries_after(object):
where where
company = %s company = %s
and actual_qty > 0 and actual_qty > 0
and is_cancelled = 0
and (serial_no = %s and (serial_no = %s
or serial_no like %s or serial_no like %s
or serial_no like %s or serial_no like %s
@ -901,6 +906,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
item_code = %s item_code = %s
AND warehouse = %s AND warehouse = %s
AND valuation_rate >= 0 AND valuation_rate >= 0
AND is_cancelled = 0
AND NOT (voucher_no = %s AND voucher_type = %s) AND NOT (voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type)) order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, warehouse, voucher_no, voucher_type))
@ -911,6 +917,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
where where
item_code = %s item_code = %s
AND valuation_rate > 0 AND valuation_rate > 0
AND is_cancelled = 0
AND NOT(voucher_no = %s AND voucher_type = %s) AND NOT(voucher_no = %s AND voucher_type = %s)
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type)) order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))

View File

@ -111,6 +111,7 @@ frappe.ui.form.on('Service Level Agreement', {
filters: [ filters: [
['DocType', 'issingle', '=', 0], ['DocType', 'issingle', '=', 0],
['DocType', 'istable', '=', 0], ['DocType', 'istable', '=', 0],
['DocType', 'is_submittable', '=', 0],
['DocType', 'name', 'not in', invalid_doctypes], ['DocType', 'name', 'not in', invalid_doctypes],
['DocType', 'module', 'not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]] ['DocType', 'module', 'not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
] ]

View File

@ -29,6 +29,7 @@ from erpnext.support.doctype.issue.issue import get_holidays
class ServiceLevelAgreement(Document): class ServiceLevelAgreement(Document):
def validate(self): def validate(self):
self.validate_selected_doctype()
self.validate_doc() self.validate_doc()
self.validate_status_field() self.validate_status_field()
self.check_priorities() self.check_priorities()
@ -106,6 +107,23 @@ class ServiceLevelAgreement(Document):
frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format( frappe.throw(_("Service Level Agreement for {0} {1} already exists.").format(
frappe.bold(self.entity_type), frappe.bold(self.entity))) frappe.bold(self.entity_type), frappe.bold(self.entity)))
def validate_selected_doctype(self):
invalid_doctypes = list(frappe.model.core_doctypes_list)
invalid_doctypes.extend(['Cost Center', 'Company'])
valid_document_types = frappe.get_all('DocType', {
'issingle': 0,
'istable': 0,
'is_submittable': 0,
'name': ['not in', invalid_doctypes],
'module': ['not in', ["Email", "Core", "Custom", "Event Streaming", "Social", "Data Migration", "Geo", "Desk"]]
}, pluck="name")
if self.document_type not in valid_document_types:
frappe.throw(
msg=_("Please select valid document type."),
title=_("Invalid Document Type")
)
def validate_status_field(self): def validate_status_field(self):
meta = frappe.get_meta(self.document_type) meta = frappe.get_meta(self.document_type)
if not meta.get_field("status"): if not meta.get_field("status"):
@ -247,8 +265,14 @@ def get_active_service_level_agreement_for(doc):
] ]
customer = doc.get('customer') customer = doc.get('customer')
if customer:
or_filters.extend([
["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)],
["Service Level Agreement", "entity_type", "is", "not set"]
])
else:
or_filters.append( or_filters.append(
["Service Level Agreement", "entity", "in", [customer] + get_customer_group(customer) + get_customer_territory(customer)] ["Service Level Agreement", "entity_type", "is", "not set"]
) )
default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]] default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
@ -361,11 +385,18 @@ def apply(doc, method=None):
sla = get_active_service_level_agreement_for(doc) sla = get_active_service_level_agreement_for(doc)
if not sla: if not sla:
remove_sla_if_applied(doc)
return return
process_sla(doc, sla) process_sla(doc, sla)
def remove_sla_if_applied(doc):
doc.service_level_agreement = None
doc.response_by = None
doc.resolution_by = None
def process_sla(doc, sla): def process_sla(doc, sla):
if not doc.creation: if not doc.creation:

View File

@ -1,13 +0,0 @@
from frappe import _
def get_data():
return {
'fieldname': 'service_level_agreement',
'transactions': [
{
'label': _('Issue'),
'items': ['Issue']
}
]
}

View File

@ -244,6 +244,13 @@ class TestServiceLevelAgreement(unittest.TestCase):
applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement') applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement')
self.assertEqual(applied_sla, lead_sla.name) self.assertEqual(applied_sla, lead_sla.name)
# check if SLA is removed if condition fails
lead.reload()
lead.source = None
lead.save()
applied_sla = frappe.db.get_value('Lead', lead.name, 'service_level_agreement')
self.assertFalse(applied_sla)
def tearDown(self): def tearDown(self):
for d in frappe.get_all("Service Level Agreement"): for d in frappe.get_all("Service Level Agreement"):
frappe.delete_doc("Service Level Agreement", d.name, force=1) frappe.delete_doc("Service Level Agreement", d.name, force=1)