diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py index 7e270601fb5..4b1535e1000 100644 --- a/erpnext/accounts/deferred_revenue.py +++ b/erpnext/accounts/deferred_revenue.py @@ -255,11 +255,13 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): enable_check = "enable_deferred_revenue" \ 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): start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date) 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": against, project = doc.customer, doc.project credit_account, debit_account = item.income_account, item.deferred_revenue_account @@ -280,6 +282,10 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): if not amount: 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: 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) @@ -407,8 +413,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, 'account': credit_account, 'credit': base_amount, 'credit_in_account_currency': amount, - 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier', - 'party': against, 'account_currency': account_currency, 'reference_name': doc.name, 'reference_type': doc.doctype, @@ -421,8 +425,6 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against, 'account': debit_account, 'debit': base_amount, 'debit_in_account_currency': amount, - 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier', - 'party': against, 'account_currency': account_currency, 'reference_name': doc.name, 'reference_type': doc.doctype, diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js index 7a1d7359488..320e1cab7c3 100644 --- a/erpnext/accounts/doctype/account/account.js +++ b/erpnext/accounts/doctype/account/account.js @@ -43,12 +43,12 @@ frappe.ui.form.on('Account', { frm.trigger('add_toolbar_buttons'); } if (frm.has_perm('write')) { - frm.add_custom_button(__('Update Account Name / Number'), function () { - frm.trigger("update_account_number"); - }); frm.add_custom_button(__('Merge Account'), function () { frm.trigger("merge_account"); - }); + }, __('Actions')); + frm.add_custom_button(__('Update Account Name / Number'), function () { + frm.trigger("update_account_number"); + }, __('Actions')); } } }, @@ -59,11 +59,12 @@ frappe.ui.form.on('Account', { } }, add_toolbar_buttons: function(frm) { - frm.add_custom_button(__('Chart of Accounts'), - function () { frappe.set_route("Tree", "Account"); }); + frm.add_custom_button(__('Chart of Accounts'), () => { + frappe.set_route("Tree", "Account"); + }, __('View')); if (frm.doc.is_group == 1) { - frm.add_custom_button(__('Group to Non-Group'), function () { + frm.add_custom_button(__('Convert to Non-Group'), function () { return frappe.call({ doc: frm.doc, method: 'convert_group_to_ledger', @@ -71,10 +72,11 @@ frappe.ui.form.on('Account', { frm.refresh(); } }); - }); + }, __('Actions')); + } else if (cint(frm.doc.is_group) == 0 && frappe.boot.user.can_read.indexOf("GL Entry") !== -1) { - frm.add_custom_button(__('Ledger'), function () { + frm.add_custom_button(__('General Ledger'), function () { frappe.route_options = { "account": frm.doc.name, "from_date": frappe.sys_defaults.year_start_date, @@ -82,9 +84,9 @@ frappe.ui.form.on('Account', { "company": frm.doc.company }; frappe.set_route("query-report", "General Ledger"); - }); + }, __('View')); - frm.add_custom_button(__('Non-Group to Group'), function () { + frm.add_custom_button(__('Convert to Group'), function () { return frappe.call({ doc: frm.doc, method: 'convert_ledger_to_group', @@ -92,7 +94,7 @@ frappe.ui.form.on('Account', { frm.refresh(); } }); - }); + }, __('Actions')); } }, diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js index f7d471b725e..dd7409f4b01 100644 --- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js +++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js @@ -7,7 +7,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", { frm.set_query("bank_account", function () { return { filters: { - company: ["in", frm.doc.company], + company: frm.doc.company, 'is_company_account': 1 }, }; diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js index 016f29a7b51..866633f74e5 100644 --- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js +++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js @@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", { "withdrawal", "description", "reference_number", - "bank_account" + "bank_account", + "currency" ], }, }); diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index d76641dc9bd..a83ea65541c 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -31,7 +31,7 @@ frappe.ui.form.on("Journal Entry", { if(frm.doc.docstatus==1) { frm.add_custom_button(__('Reverse Journal Entry'), function() { return erpnext.journal_entry.reverse_journal_entry(frm); - }, __('Make')); + }, __('Actions')); } if (frm.doc.__islocal) { diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json index 20678d787b4..335fd350def 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.json +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json @@ -13,6 +13,7 @@ "voucher_type", "naming_series", "finance_book", + "reversal_of", "tax_withholding_category", "column_break1", "from_template", @@ -515,13 +516,21 @@ "fieldname": "apply_tds", "fieldtype": "Check", "label": "Apply Tax Withholding Amount " + }, + { + "depends_on": "eval:doc.docstatus", + "fieldname": "reversal_of", + "fieldtype": "Link", + "label": "Reversal Of", + "options": "Journal Entry", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 176, "is_submittable": 1, "links": [], - "modified": "2021-09-09 15:31:14.484029", + "modified": "2022-01-04 13:39:36.485954", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry", diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py index 3aed3c89d53..9c1710217dd 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.py +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py @@ -397,13 +397,14 @@ class JournalEntry(AccountsController): debit_or_credit = 'Debit' if d.debit else 'Credit' party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no, debit_or_credit) + against_voucher = ['', against_voucher[1]] else: if d.reference_type == "Sales Invoice": party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1] else: 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}") .format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1], d.reference_type, d.reference_name)) @@ -468,13 +469,22 @@ class JournalEntry(AccountsController): def set_against_account(self): accounts_debited, accounts_credited = [], [] - for d in self.get("accounts"): - 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 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' - for d in self.get("accounts"): - if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) - if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) + d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field) + else: + for d in self.get("accounts"): + 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) + + for d in self.get("accounts"): + if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited))) + if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited))) def validate_debit_credit_amount(self): for d in self.get('accounts'): @@ -1147,9 +1157,8 @@ def make_inter_company_journal_entry(name, voucher_type, company): def make_reverse_journal_entry(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc - def update_accounts(source, target, source_parent): - target.reference_type = "Journal Entry" - target.reference_name = source_parent.name + def post_process(source, target): + target.reversal_of = source.name doclist = get_mapped_doc("Journal Entry", source_name, { "Journal Entry": { @@ -1167,9 +1176,8 @@ def make_reverse_journal_entry(source_name, target_doc=None): "debit": "credit", "credit_in_account_currency": "debit_in_account_currency", "credit": "debit", - }, - "postprocess": update_accounts, + } }, - }, target_doc) + }, target_doc, post_process) return doclist diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json index bc9241802da..daee8f8c1ab 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json @@ -75,7 +75,7 @@ ], "hide_toolbar": 1, "issingle": 1, - "modified": "2019-07-25 14:57:33.187689", + "modified": "2022-01-04 15:25:06.053187", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool", diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py index 222589180ee..a8d7bf7a0e7 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py @@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document): frappe.scrub(row.party_type): row.party, "is_pos": 0, "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice", - "update_stock": 0 + "update_stock": 0, + "invoice_number": row.invoice_number }) accounting_dimension = get_accounting_dimensions() @@ -200,10 +201,13 @@ def start_import(invoices): names = [] for idx, d in enumerate(invoices): try: + invoice_number = None + if d.invoice_number: + invoice_number = d.invoice_number publish(idx, len(invoices), d.doctype) doc = frappe.get_doc(d) doc.flags.ignore_mandatory = True - doc.insert() + doc.insert(set_name=invoice_number) doc.submit() frappe.db.commit() names.append(doc.name) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py index 2da7425f9de..b5aae9845b6 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py @@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): if not frappe.db.exists("Company", "_Test Opening Invoice Company"): make_company() - def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None): + def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None): doc = frappe.get_single("Opening Invoice Creation Tool") args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company, - party_1=party_1, party_2=party_2) + party_1=party_1, party_2=party_2, invoice_number=invoice_number) doc.update(args) return doc.make_invoices() @@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase): # teardown frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account) + def test_renaming_of_invoice_using_invoice_number_field(self): + company = "_Test Opening Invoice Company" + party_1, party_2 = make_customer("Customer A"), make_customer("Customer B") + self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11") + + sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name") + sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name") + self.assertEqual(sales_inv1, "TEST-NEW-INV-11") + + #teardown + for inv in [sales_inv1, sales_inv2]: + doc = frappe.get_doc('Sales Invoice', inv) + doc.cancel() + def get_opening_invoice_creation_dict(**args): party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier" company = args.get("company", "_Test Company") @@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args): "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", - "temporary_opening_account": get_temporary_opening_account(company) + "temporary_opening_account": get_temporary_opening_account(company), + "invoice_number": args.get("invoice_number") }, { "qty": 2.0, @@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args): "item_name": "Opening Item", "due_date": "2016-09-10", "posting_date": "2016-09-05", - "temporary_opening_account": get_temporary_opening_account(company) + "temporary_opening_account": get_temporary_opening_account(company), + "invoice_number": None } ] }) diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json index 07c78631358..8149d594cb4 100644 --- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json +++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json @@ -1,9 +1,11 @@ { + "actions": [], "creation": "2017-08-29 04:26:36.159247", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "invoice_number", "party_type", "party", "temporary_opening_account", @@ -114,7 +116,7 @@ ], "istable": 1, "links": [], - "modified": "2021-12-17 19:25:06.053187", + "modified": "2022-01-04 18:40:15.927675", "modified_by": "Administrator", "module": "Accounts", "name": "Opening Invoice Creation Tool Item", diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index 61fa194aefd..80f61d4f02e 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -3,6 +3,7 @@ import json +from functools import reduce import frappe from frappe import ValidationError, _, scrub, throw @@ -1524,6 +1525,10 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= pe.received_amount = received_amount pe.letter_head = doc.get("letter_head") + if dt in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice']: + pe.project = (doc.get('project') or + reduce(lambda prev,cur: prev or cur, [x.get('project') for x in doc.get('items')], None)) # get first non-empty project from items + if pe.party_type in ["Customer", "Supplier"]: bank_account = get_party_bank_account(pe.party_type, pe.party) pe.set("bank_account", bank_account) @@ -1709,7 +1714,10 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta def apply_early_payment_discount(paid_amount, received_amount, doc): total_discount = 0 - if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule: + eligible_for_payments = ['Sales Order', 'Sales Invoice', 'Purchase Order', 'Purchase Invoice'] + has_payment_schedule = hasattr(doc, 'payment_schedule') and doc.payment_schedule + + if doc.doctype in eligible_for_payments and has_payment_schedule: for term in doc.payment_schedule: if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date: if term.discount_type == 'Percentage': diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 814372f6b35..de31b5c4136 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -16,6 +16,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_multi_mode_option, ) from erpnext.accounts.party import get_due_date, get_party_account +from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos @@ -125,9 +126,26 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.") .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) elif invalid_serial_nos: - frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.") + frappe.throw(_("Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no.") .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) + def validate_pos_reserved_batch_qty(self, item): + filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no":item.batch_no} + + available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code) + reserved_batch_qty = get_pos_reserved_batch_qty(filters) + + bold_item_name = frappe.bold(item.item_name) + bold_extra_batch_qty_needed = frappe.bold(abs(available_batch_qty - reserved_batch_qty - item.qty)) + bold_invalid_batch_no = frappe.bold(item.batch_no) + + if (available_batch_qty - reserved_batch_qty) == 0: + frappe.throw(_("Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no.") + .format(item.idx, bold_invalid_batch_no, bold_item_name), title=_("Item Unavailable")) + elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: + frappe.throw(_("Row #{}: Batch No. {} of item {} has less than required stock available, {} more required") + .format(item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed), title=_("Item Unavailable")) + def validate_delivered_serial_nos(self, item): serial_nos = get_serial_nos(item.serial_no) delivered_serial_nos = frappe.db.get_list('Serial No', { @@ -150,6 +168,8 @@ class POSInvoice(SalesInvoice): if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) + elif d.batch_no: + self.validate_pos_reserved_batch_qty(d) else: if allow_negative_stock: return @@ -334,7 +354,6 @@ class POSInvoice(SalesInvoice): if not for_validate and not self.customer: self.customer = profile.customer - self.ignore_pricing_rule = profile.ignore_pricing_rule self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount self.set_warehouse = profile.get('warehouse') or self.set_warehouse diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 66963335376..56479a0b77d 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -521,6 +521,72 @@ class TestPOSInvoice(unittest.TestCase): rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") self.assertEqual(rounded_total, 400) + def test_pos_batch_item_qty_validation(self): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_batch_item_with_batch, + ) + create_batch_item_with_batch('_BATCH ITEM', 'TestBatch 01') + item = frappe.get_doc('Item', '_BATCH ITEM') + batch = frappe.get_doc('Batch', 'TestBatch 01') + batch.submit() + item.batch_no = 'TestBatch 01' + item.save() + + se = make_stock_entry(target="_Test Warehouse - _TC", item_code="_BATCH ITEM", qty=2, basic_rate=100, batch_no='TestBatch 01') + + pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1) + pos_inv1.items[0].batch_no = 'TestBatch 01' + pos_inv1.save() + pos_inv1.submit() + + pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1) + pos_inv2.items[0].batch_no = 'TestBatch 01' + pos_inv2.save() + + self.assertRaises(frappe.ValidationError, pos_inv2.submit) + + #teardown + pos_inv1.reload() + pos_inv1.cancel() + pos_inv1.delete() + pos_inv2.reload() + pos_inv2.delete() + se.cancel() + batch.reload() + batch.cancel() + batch.delete() + + def test_ignore_pricing_rule(self): + from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule + + item_price = frappe.get_doc({ + 'doctype': 'Item Price', + 'item_code': '_Test Item', + 'price_list': '_Test Price List', + 'price_list_rate': '450', + }) + item_price.insert() + pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10) + pr.save() + pos_inv = create_pos_invoice(qty=1, do_not_submit=1) + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.items[0].discount_percentage, 10) + # rate shouldn't change + self.assertEquals(pos_inv.items[0].rate, 405) + + pos_inv.ignore_pricing_rule = 1 + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.ignore_pricing_rule, 1) + # rate should change since pricing rules are ignored + self.assertEquals(pos_inv.items[0].rate, 300) + + item_price.delete() + pos_inv.delete() + pr.delete() + + def create_pos_invoice(**args): args = frappe._dict(args) pos_profile = None @@ -557,7 +623,8 @@ def create_pos_invoice(**args): "income_account": args.income_account or "Sales - _TC", "expense_account": args.expense_account or "Cost of Goods Sold - _TC", "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no + "serial_no": args.serial_no, + "batch_no": args.batch_no }) if not args.do_not_save: @@ -570,3 +637,8 @@ def create_pos_invoice(**args): pos_inv.payment_schedule = [] return pos_inv + +def make_batch_item(item_name): + from erpnext.stock.doctype.item.test_item import make_item + if not frappe.db.exists(item_name): + return make_item(item_name, dict(has_batch_no = 1, create_new_batch = 1, is_stock_item=1)) \ No newline at end of file diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 74e188471de..e9a018ec46c 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -652,7 +652,7 @@ def make_pricing_rule(**args): "rate": args.rate or 0.0, "margin_rate_or_amount": args.margin_rate_or_amount or 0.0, "condition": args.condition or '', - "priority": 1, + "priority": args.priority or 1, "discount_amount": args.discount_amount or 0.0, "apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 }) @@ -678,6 +678,8 @@ def make_pricing_rule(**args): if args.get(applicable_for): doc.db_set(applicable_for, args.get(applicable_for)) + return doc + def setup_pricing_rule_data(): if not frappe.db.exists('Campaign', '_Test Campaign'): frappe.get_doc({ diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 33fbd748c7b..c5cc05749a7 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -503,11 +503,11 @@ class PurchaseInvoice(BuyingController): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total) + else self.base_grand_total, self.precision("base_grand_total")) if grand_total and not self.is_internal_transfer(): # Did not use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) gl_entries.append( self.get_gl_dict({ "account": self.credit_to, @@ -515,8 +515,8 @@ class PurchaseInvoice(BuyingController): "party": self.supplier, "due_date": self.due_date, "against": self.against_expense_account, - "credit": grand_total_in_company_currency, - "credit_in_account_currency": grand_total_in_company_currency \ + "credit": base_grand_total, + "credit_in_account_currency": base_grand_total \ if self.party_account_currency==self.company_currency else grand_total, "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 480d09fb0b3..d01baebe636 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -1213,7 +1213,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def update_tax_witholding_category(company, account): from erpnext.accounts.utils import get_fiscal_year - fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021') + fiscal_year = get_fiscal_year(date=nowdate()) if not frappe.db.get_value('Tax Withholding Rate', {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 545abf77e6b..5062c1c807a 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -651,7 +651,7 @@ "hide_seconds": 1, "label": "Ignore Pricing Rule", "no_copy": 1, - "permlevel": 1, + "permlevel": 0, "print_hide": 1 }, { @@ -2038,7 +2038,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-10-21 20:19:38.667508", + "modified": "2021-12-23 20:19:38.667508", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 176d47897d6..772e8c4e872 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -879,11 +879,11 @@ class SalesInvoice(SellingController): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + base_grand_total = flt(self.base_rounded_total if (self.base_rounding_adjustment and self.base_rounded_total) + else self.base_grand_total, self.precision("base_grand_total")) + if grand_total and not self.is_internal_transfer(): # Didnot use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) - gl_entries.append( self.get_gl_dict({ "account": self.debit_to, @@ -891,8 +891,8 @@ class SalesInvoice(SellingController): "party": self.customer, "due_date": self.due_date, "against": self.against_income_account, - "debit": grand_total_in_company_currency, - "debit_in_account_currency": grand_total_in_company_currency \ + "debit": base_grand_total, + "debit_in_account_currency": base_grand_total \ if self.party_account_currency==self.company_currency else grand_total, "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, "against_voucher_type": self.doctype, diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 6696d4ad2a8..12e00dcac6f 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp from erpnext.accounts.utils import PaymentEntryUnlinkError from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data +from erpnext.controllers.accounts_controller import update_invoice_status from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.regional.india.utils import get_ewb_data @@ -1788,47 +1789,6 @@ class TestSalesInvoice(unittest.TestCase): 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): deferred_account = create_account(account_name="Deferred Revenue", parent_account="Current Liabilities - _TC", company="_Test Company") @@ -2358,6 +2318,41 @@ class TestSalesInvoice(unittest.TestCase): si.reload() self.assertEqual(si.status, "Paid") + def test_update_invoice_status(self): + today = nowdate() + + # Sales Invoice without Payment Schedule + si = create_sales_invoice(posting_date=add_days(today, -5)) + + # Sales Invoice with Payment Schedule + si_with_payment_schedule = create_sales_invoice(do_not_submit=True) + si_with_payment_schedule.extend("payment_schedule", [ + { + "due_date": add_days(today, -5), + "invoice_portion": 50, + "payment_amount": si_with_payment_schedule.grand_total / 2 + }, + { + "due_date": add_days(today, 5), + "invoice_portion": 50, + "payment_amount": si_with_payment_schedule.grand_total / 2 + } + ]) + si_with_payment_schedule.submit() + + + for invoice in (si, si_with_payment_schedule): + invoice.db_set("status", "Unpaid") + update_invoice_status() + invoice.reload() + self.assertEqual(invoice.status, "Overdue") + + invoice.db_set("status", "Unpaid and Discounted") + update_invoice_status() + invoice.reload() + self.assertEqual(invoice.status, "Overdue and Discounted") + + def test_sales_commission(self): si = frappe.copy_doc(test_records[0]) item = copy.deepcopy(si.get('items')[0]) @@ -2419,6 +2414,74 @@ class TestSalesInvoice(unittest.TestCase): 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(): si = make_sales_invoice_for_ewaybill() si.naming_series = 'INV-2020-.#####' diff --git a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json index d2c505c6300..e032bb307b0 100644 --- a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json +++ b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json @@ -28,14 +28,14 @@ { "columns": 2, "fieldname": "single_threshold", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 1, "label": "Single Transaction Threshold" }, { "columns": 3, "fieldname": "cumulative_threshold", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 1, "label": "Cumulative Transaction Threshold" }, @@ -59,7 +59,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-31 11:42:12.213977", + "modified": "2022-01-13 12:04:42.904263", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withholding Rate", @@ -68,5 +68,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 305cddb102a..715cd6476e8 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { "label": __("Show Future Payments"), "fieldtype": "Check", }, + { + "fieldname":"show_gl_balance", + "label": __("Show GL Balance"), + "fieldtype": "Check", + }, ], onload: function(report) { diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index a95bcf83ef7..4559fa94a4a 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -4,7 +4,7 @@ import frappe from frappe import _, scrub -from frappe.utils import cint +from frappe.utils import cint, flt from six import iteritems from erpnext.accounts.party import get_partywise_advanced_payment_amount @@ -37,6 +37,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): party_advance_amount = get_partywise_advanced_payment_amount(self.party_type, self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {} + if self.filters.show_gl_balance: + gl_balance_map = get_gl_balance(self.filters.report_date) + for party, party_dict in iteritems(self.party_total): if party_dict.outstanding == 0: continue @@ -56,6 +59,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): # but in summary report advance shown in separate column row.paid -= row.advance + if self.filters.show_gl_balance: + row.gl_balance = gl_balance_map.get(party) + row.diff = flt(row.outstanding) - flt(row.gl_balance) + self.data.append(row) def get_party_total(self, args): @@ -115,6 +122,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.add_column(_(credit_debit_label), fieldname='credit_note') self.add_column(_('Outstanding Amount'), fieldname='outstanding') + if self.filters.show_gl_balance: + self.add_column(_('GL Balance'), fieldname='gl_balance') + self.add_column(_('Difference'), fieldname='diff') + self.setup_ageing_columns() if self.party_type == "Customer": @@ -141,3 +152,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): # Add column for total due amount self.add_column(label="Total Amount Due", fieldname='total_due') + +def get_gl_balance(report_date): + return frappe._dict(frappe.db.get_all("GL Entry", fields=['party', 'sum(debit - credit)'], + filters={'posting_date': ("<=", report_date), 'is_cancelled': 0}, group_by='party', as_list=1)) diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py index a4842c1844f..3a51db8a97f 100644 --- a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -121,20 +121,21 @@ class Deferred_Item(object): """ simulate future posting by creating dummy gl entries. starts from the last posting date. """ - if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date: - self.estimate_for_period_list = get_period_list( - self.filters.from_fiscal_year, - self.filters.to_fiscal_year, - add_days(self.last_entry_date, 1), - self.period_list[-1].to_date, - "Date Range", - "Monthly", - company=self.filters.company, - ) - for period in self.estimate_for_period_list: - amount = self.calculate_amount(period.from_date, period.to_date) - gle = self.make_dummy_gle(period.key, period.to_date, amount) - self.gle_entries.append(gle) + if self.service_start_date != self.service_end_date: + if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date: + self.estimate_for_period_list = get_period_list( + self.filters.from_fiscal_year, + self.filters.to_fiscal_year, + add_days(self.last_entry_date, 1), + self.period_list[-1].to_date, + "Date Range", + "Monthly", + company=self.filters.company, + ) + for period in self.estimate_for_period_list: + amount = self.calculate_amount(period.from_date, period.to_date) + gle = self.make_dummy_gle(period.key, period.to_date, amount) + self.gle_entries.append(gle) def calculate_item_revenue_expense_for_period(self): """ diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index b2968761c63..010284c2ea5 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = { "fieldname": "include_dimensions", "label": __("Consider Accounting Dimensions"), "fieldtype": "Check", - "default": 0 + "default": 1 }, { "fieldname": "show_opening_entries", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 9aad52137b4..452a60d3055 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -449,9 +449,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): elif group_by_voucher_consolidated: keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] - for dim in accounting_dimensions: - keylist.append(gle.get(dim)) - keylist.append(gle.get("cost_center")) + if filters.get("include_dimensions"): + for dim in accounting_dimensions: + keylist.append(gle.get(dim)) + keylist.append(gle.get("cost_center")) + key = tuple(keylist) if key not in consolidated_gle: consolidated_gle.setdefault(key, gle) @@ -595,14 +597,14 @@ def get_columns(filters): "fieldname": dim.fieldname, "width": 100 }) - - columns.extend([ - { + columns.append({ "label": _("Cost Center"), "options": "Cost Center", "fieldname": "cost_center", "width": 100 - }, + }) + + columns.extend([ { "label": _("Against Voucher Type"), "fieldname": "against_voucher_type", diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 90257c784ee..a57a3e281d3 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -583,7 +583,17 @@ class Asset(AccountsController): return purchase_document def get_fixed_asset_account(self): - return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + fixed_asset_account = get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company) + if not fixed_asset_account: + frappe.throw( + _("Set {0} in asset category {1} for company {2}").format( + frappe.bold("Fixed Asset Account"), + frappe.bold(self.asset_category), + frappe.bold(self.company), + ), + title=_("Account not Found"), + ) + return fixed_asset_account def get_cwip_account(self, cwip_enabled=False): cwip_account = None diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 28cded2e671..b7b198eac4c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -7,6 +7,7 @@ import json import frappe from frappe import _, throw from frappe.model.workflow import get_workflow_name, is_transition_condition_satisfied +from frappe.query_builder.functions import Sum from frappe.utils import ( add_days, add_months, @@ -113,7 +114,7 @@ class AccountsController(TransactionBase): _('{0} is blocked so this transaction cannot proceed').format(supplier_name), raise_exception=1) def validate(self): - if not self.get('is_return'): + if not self.get('is_return') and not self.get('is_debit_note'): self.validate_qty_is_not_zero() if self.get("_action") and self._action != "update_after_submit": @@ -190,8 +191,6 @@ class AccountsController(TransactionBase): 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): 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): self.validate_payment_schedule_dates() @@ -1692,58 +1691,69 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, def update_invoice_status(): """Updates status as Overdue for applicable invoices. Runs daily.""" today = getdate() - + payment_schedule = frappe.qb.DocType("Payment Schedule") for doctype in ("Sales Invoice", "Purchase Invoice"): - frappe.db.sql(""" - UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue' - WHERE invoice.docstatus = 1 - AND invoice.status REGEXP '^Unpaid|^Partly Paid' - AND invoice.outstanding_amount > 0 - AND ( - {or_condition} - ( - ( - CASE - WHEN invoice.party_account_currency = invoice.currency - THEN ( - CASE - WHEN invoice.disable_rounded_total - THEN invoice.grand_total - ELSE invoice.rounded_total - END - ) - ELSE ( - CASE - WHEN invoice.disable_rounded_total - THEN invoice.base_grand_total - ELSE invoice.base_rounded_total - END - ) - END - ) - invoice.outstanding_amount - ) < ( - SELECT SUM( - CASE - WHEN invoice.party_account_currency = invoice.currency - THEN ps.payment_amount - ELSE ps.base_payment_amount - END - ) - FROM `tabPayment Schedule` ps - WHERE ps.parent = invoice.name - AND ps.due_date < %(today)s - ) - ) - """.format( - doctype=doctype, - or_condition=( - "invoice.is_pos AND invoice.due_date < %(today)s OR" - if doctype == "Sales Invoice" - else "" - ) - ), {"today": today} + invoice = frappe.qb.DocType(doctype) + + consider_base_amount = invoice.party_account_currency != invoice.currency + payment_amount = ( + frappe.qb.terms.Case() + .when(consider_base_amount, payment_schedule.base_payment_amount) + .else_(payment_schedule.payment_amount) ) + payable_amount = ( + frappe.qb.from_(payment_schedule) + .select(Sum(payment_amount)) + .where( + (payment_schedule.parent == invoice.name) + & (payment_schedule.due_date < today) + ) + ) + + total = ( + frappe.qb.terms.Case() + .when(invoice.disable_rounded_total, invoice.grand_total) + .else_(invoice.rounded_total) + ) + + base_total = ( + frappe.qb.terms.Case() + .when(invoice.disable_rounded_total, invoice.base_grand_total) + .else_(invoice.base_rounded_total) + ) + + total_amount = ( + frappe.qb.terms.Case() + .when(consider_base_amount, base_total) + .else_(total) + ) + + is_overdue = total_amount - invoice.outstanding_amount < payable_amount + + conditions = ( + (invoice.docstatus == 1) + & (invoice.outstanding_amount > 0) + & ( + invoice.status.like("Unpaid%") + | invoice.status.like("Partly Paid%") + ) + & ( + ((invoice.is_pos & invoice.due_date < today) | is_overdue) + if doctype == "Sales Invoice" + else is_overdue + ) + ) + + status = ( + frappe.qb.terms.Case() + .when(invoice.status.like("%Discounted"), "Overdue and Discounted") + .else_("Overdue") + ) + + frappe.qb.update(invoice).set("status", status).where(conditions).run() + + @frappe.whitelist() def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None): if not terms_template: @@ -2113,6 +2123,11 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil parent.update_status_updater() else: parent.check_credit_limit() + + # reset index of child table + for idx, row in enumerate(parent.get(child_docname), start=1): + row.idx = idx + parent.save() if parent_doctype == 'Purchase Order': diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py index cc773b75963..4ff851d7f94 100644 --- a/erpnext/controllers/selling_controller.py +++ b/erpnext/controllers/selling_controller.py @@ -385,7 +385,7 @@ class SellingController(StockController): # Get incoming rate based on original item cost based on valuation method qty = flt(d.get('stock_qty') or d.get('actual_qty')) - if not d.incoming_rate: + if not (self.get("is_return") and d.incoming_rate): d.incoming_rate = get_incoming_rate({ "item_code": d.item_code, "warehouse": d.warehouse, diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py index 7073e32f536..f22669b2555 100644 --- a/erpnext/controllers/stock_controller.py +++ b/erpnext/controllers/stock_controller.py @@ -17,7 +17,7 @@ from erpnext.accounts.general_ledger import ( from erpnext.accounts.utils import get_fiscal_year from erpnext.controllers.accounts_controller import AccountsController from erpnext.stock import get_warehouse_account_map -from erpnext.stock.stock_ledger import get_items_to_be_repost, get_valuation_rate +from erpnext.stock.stock_ledger import get_items_to_be_repost class QualityInspectionRequiredError(frappe.ValidationError): pass @@ -111,17 +111,6 @@ class StockController(AccountsController): self.check_expense_account(item_row) - # If the item does not have the allow zero valuation rate flag set - # and ( valuation rate not mentioned in an incoming entry - # or incoming entry not found while delivering the item), - # try to pick valuation rate from previous sle or Item master and update in SLE - # Otherwise, throw an exception - - if not sle.stock_value_difference and self.doctype != "Stock Reconciliation" \ - and not item_row.get("allow_zero_valuation_rate"): - - sle = self.update_stock_ledger_entries(sle) - # expense account/ target_warehouse / source_warehouse if item_row.get('target_warehouse'): warehouse = item_row.get('target_warehouse') @@ -164,26 +153,6 @@ class StockController(AccountsController): return frappe.flags.debit_field_precision - def update_stock_ledger_entries(self, sle): - sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse, - self.doctype, self.name, currency=self.company_currency, company=self.company) - - sle.stock_value = flt(sle.qty_after_transaction) * flt(sle.valuation_rate) - sle.stock_value_difference = flt(sle.actual_qty) * flt(sle.valuation_rate) - - if sle.name: - frappe.db.sql(""" - update - `tabStock Ledger Entry` - set - stock_value = %(stock_value)s, - valuation_rate = %(valuation_rate)s, - stock_value_difference = %(stock_value_difference)s - where - name = %(name)s""", (sle)) - - return sle - def get_voucher_details(self, default_expense_account, default_cost_center, sle_map): if self.doctype == "Stock Reconciliation": reconciliation_purpose = frappe.db.get_value(self.doctype, self.name, "purpose") diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index b9c95508152..02b1b3b1734 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -139,6 +139,8 @@ class calculate_taxes_and_totals(object): if not item.qty and self.doc.get("is_return"): item.amount = flt(-1 * item.rate, item.precision("amount")) + elif not item.qty and self.doc.get("is_debit_note"): + item.amount = flt(item.rate, item.precision("amount")) else: item.amount = flt(item.rate * item.qty, item.precision("amount")) @@ -594,13 +596,14 @@ class calculate_taxes_and_totals(object): if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: grand_total = self.doc.rounded_total or self.doc.grand_total + base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total + if self.doc.party_account_currency == self.doc.currency: total_amount_to_pay = flt(grand_total - self.doc.total_advance - flt(self.doc.write_off_amount), self.doc.precision("grand_total")) else: - total_amount_to_pay = flt(flt(grand_total * - self.doc.conversion_rate, self.doc.precision("grand_total")) - self.doc.total_advance - - flt(self.doc.base_write_off_amount), self.doc.precision("grand_total")) + total_amount_to_pay = flt(flt(base_grand_total, self.doc.precision("base_grand_total")) - self.doc.total_advance + - flt(self.doc.base_write_off_amount), self.doc.precision("base_grand_total")) self.doc.round_floats_in(self.doc, ["paid_amount"]) change_amount = 0 diff --git a/erpnext/controllers/tests/test_queries.py b/erpnext/controllers/tests/test_queries.py index 05541d16887..908d78c15bf 100644 --- a/erpnext/controllers/tests/test_queries.py +++ b/erpnext/controllers/tests/test_queries.py @@ -1,6 +1,8 @@ import unittest from functools import partial +import frappe + from erpnext.controllers import queries @@ -85,3 +87,6 @@ class TestQueries(unittest.TestCase): wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]]) self.assertGreaterEqual(len(wh), 1) + + def test_default_uoms(self): + self.assertGreaterEqual(frappe.db.count("UOM", {"enabled": 1}), 10) diff --git a/erpnext/controllers/tests/test_transaction_base.py b/erpnext/controllers/tests/test_transaction_base.py index 13aa697610e..f4d3f97ef0d 100644 --- a/erpnext/controllers/tests/test_transaction_base.py +++ b/erpnext/controllers/tests/test_transaction_base.py @@ -4,19 +4,72 @@ import frappe class TestUtils(unittest.TestCase): - def test_reset_default_field_value(self): - doc = frappe.get_doc({ - "doctype": "Purchase Receipt", - "set_warehouse": "Warehouse 1", - }) + def test_reset_default_field_value(self): + doc = frappe.get_doc({ + "doctype": "Purchase Receipt", + "set_warehouse": "Warehouse 1", + }) - # Same values - doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] - doc.reset_default_field_value("set_warehouse", "items", "warehouse") - self.assertEqual(doc.set_warehouse, "Warehouse 1") + # Same values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, "Warehouse 1") - # Mixed values - doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] - doc.reset_default_field_value("set_warehouse", "items", "warehouse") - self.assertEqual(doc.set_warehouse, None) + # Mixed values + doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}] + doc.reset_default_field_value("set_warehouse", "items", "warehouse") + self.assertEqual(doc.set_warehouse, None) + def test_reset_default_field_value_in_mfg_stock_entry(self): + # manufacture stock entry with rows having blank source/target wh + se = frappe.get_doc( + doctype="Stock Entry", + purpose="Manufacture", + stock_entry_type="Manufacture", + company="_Test Company", + from_warehouse="_Test Warehouse - _TC", + to_warehouse="_Test Warehouse 1 - _TC", + items=[ + frappe._dict(item_code="_Test Item", qty=1, basic_rate=200, s_warehouse="_Test Warehouse - _TC"), + frappe._dict(item_code="_Test FG Item", qty=4, t_warehouse="_Test Warehouse 1 - _TC", is_finished_item=1) + ] + ) + se.save() + + # default fields must be untouched + self.assertEqual(se.from_warehouse, "_Test Warehouse - _TC") + self.assertEqual(se.to_warehouse, "_Test Warehouse 1 - _TC") + + se.delete() + + def test_reset_default_field_value_in_transfer_stock_entry(self): + doc = frappe.get_doc({ + "doctype": "Stock Entry", + "purpose": "Material Receipt", + "from_warehouse": "Warehouse 1", + "to_warehouse": "Warehouse 2", + }) + + # Same values + doc.items = [ + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"} + ] + + doc.reset_default_field_value("from_warehouse", "items", "s_warehouse") + doc.reset_default_field_value("to_warehouse", "items", "t_warehouse") + self.assertEqual(doc.from_warehouse, "Warehouse 1") + self.assertEqual(doc.to_warehouse, "Warehouse 2") + + # Mixed values in source wh + doc.items = [ + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 3", "t_warehouse": "Warehouse 2"}, + {"s_warehouse": "Warehouse 1", "t_warehouse": "Warehouse 2"} + ] + + doc.reset_default_field_value("from_warehouse", "items", "s_warehouse") + doc.reset_default_field_value("to_warehouse", "items", "t_warehouse") + self.assertEqual(doc.from_warehouse, None) + self.assertEqual(doc.to_warehouse, "Warehouse 2") \ No newline at end of file diff --git a/erpnext/e_commerce/doctype/website_item/website_item.py b/erpnext/e_commerce/doctype/website_item/website_item.py index 2e60dfd9459..181aaa1292e 100644 --- a/erpnext/e_commerce/doctype/website_item/website_item.py +++ b/erpnext/e_commerce/doctype/website_item/website_item.py @@ -1,7 +1,6 @@ # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -import itertools import json import frappe @@ -203,16 +202,15 @@ class WebsiteItem(WebsiteGenerator): context.body_class = "product-page" context.parents = get_parent_item_groups(self.item_group, from_item=True) # breadcumbs - self.attributes = frappe.get_all("Item Variant Attribute", + self.attributes = frappe.get_all( + "Item Variant Attribute", fields=["attribute", "attribute_value"], - filters={"parent": self.item_code}) + filters={"parent": self.item_code} + ) if self.slideshow: context.update(get_slideshow(self)) - self.set_variant_context(context) - self.set_attribute_context(context) - self.set_disabled_attributes(context) self.set_metatags(context) self.set_shopping_cart_data(context) @@ -237,61 +235,6 @@ class WebsiteItem(WebsiteGenerator): return context - def set_variant_context(self, context): - if not self.has_variants: - return - - context.no_cache = True - variant = frappe.form_dict.variant - - # load variants - # also used in set_attribute_context - context.variants = frappe.get_all( - "Item", - filters={ - "variant_of": self.item_code, - "published_in_website": 1 - }, - order_by="name asc") - - # the case when the item is opened for the first time from its list - if not variant and context.variants: - variant = context.variants[0] - - if variant: - context.variant = frappe.get_doc("Item", variant) - fields = ("website_image", "website_image_alt", "web_long_description", "description", - "website_specifications") - - for fieldname in fields: - if context.variant.get(fieldname): - value = context.variant.get(fieldname) - if isinstance(value, list): - value = [d.as_dict() for d in value] - - context[fieldname] = value - - if self.slideshow and context.variant and context.variant.slideshow: - context.update(get_slideshow(context.variant)) - - - def set_attribute_context(self, context): - if not self.has_variants: - return - - attribute_values_available = {} - context.attribute_values = {} - context.selected_attributes = {} - - # load attributes - self.set_selected_attributes(context.variants, context, attribute_values_available) - - # filter attributes, order based on attribute table - item = frappe.get_cached_doc("Item", self.item_code) - self.set_attribute_values(item.attributes, context, attribute_values_available) - - context.variant_info = json.dumps(context.variants) - def set_selected_attributes(self, variants, context, attribute_values_available): for variant in variants: variant.attributes = frappe.get_all( @@ -328,50 +271,6 @@ class WebsiteItem(WebsiteGenerator): if attr_value.attribute_value in attribute_values_available.get(attr.attribute, []): values.append(attr_value.attribute_value) - def set_disabled_attributes(self, context): - """Disable selection options of attribute combinations that do not result in a variant""" - - if not self.attributes or not self.has_variants: - return - - context.disabled_attributes = {} - attributes = [attr.attribute for attr in self.attributes] - - def find_variant(combination): - for variant in context.variants: - if len(variant.attributes) < len(attributes): - continue - - if "combination" not in variant: - ref_combination = [] - - for attr in variant.attributes: - idx = attributes.index(attr.attribute) - ref_combination.insert(idx, attr.attribute_value) - - variant["combination"] = ref_combination - - if not (set(combination) - set(variant["combination"])): - # check if the combination is a subset of a variant combination - # eg. [Blue, 0.5] is a possible combination if exists [Blue, Large, 0.5] - return True - - for i, attr in enumerate(self.attributes): - if i == 0: - continue - - combination_source = [] - - # loop through previous attributes - for prev_attr in self.attributes[:i]: - combination_source.append([context.selected_attributes.get(prev_attr.attribute)]) - - combination_source.append(context.attribute_values[attr.attribute]) - - for combination in itertools.product(*combination_source): - if not find_variant(combination): - context.disabled_attributes.setdefault(attr.attribute, []).append(combination[-1]) - def set_metatags(self, context): context.metatags = frappe._dict({}) diff --git a/erpnext/e_commerce/product_data_engine/query.py b/erpnext/e_commerce/product_data_engine/query.py index ddd9987bb92..b927b0146e1 100644 --- a/erpnext/e_commerce/product_data_engine/query.py +++ b/erpnext/e_commerce/product_data_engine/query.py @@ -197,7 +197,10 @@ class ProductQuery: website_item_groups = frappe.db.get_all( "Website Item", fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"], - filters=[["Website Item Group", "item_group", "=", item_group]] + filters=[ + ["Website Item Group", "item_group", "=", item_group], + ["published", "=", 1] + ] ) return website_item_groups diff --git a/erpnext/e_commerce/variant_selector/item_variants_cache.py b/erpnext/e_commerce/variant_selector/item_variants_cache.py index 39eb9155d5e..bb6b3ef37fe 100644 --- a/erpnext/e_commerce/variant_selector/item_variants_cache.py +++ b/erpnext/e_commerce/variant_selector/item_variants_cache.py @@ -44,7 +44,7 @@ class ItemVariantsCacheManager: val = frappe.cache().get_value('ordered_attribute_values_map') if val: return val - all_attribute_values = frappe.db.get_all('Item Attribute Value', + all_attribute_values = frappe.get_all('Item Attribute Value', ['attribute_value', 'idx', 'parent'], order_by='idx asc') ordered_attribute_values_map = frappe._dict({}) @@ -57,25 +57,34 @@ class ItemVariantsCacheManager: def build_cache(self): parent_item_code = self.item_code - attributes = [a.attribute for a in frappe.db.get_all('Item Variant Attribute', - {'parent': parent_item_code}, ['attribute'], order_by='idx asc') + attributes = [ + a.attribute for a in frappe.get_all( + 'Item Variant Attribute', + {'parent': parent_item_code}, + ['attribute'], + order_by='idx asc' + ) ] - item_variants_data = frappe.db.get_all('Item Variant Attribute', - {'variant_of': parent_item_code}, ['parent', 'attribute', 'attribute_value'], + # join with Website Item + item_variants_data = frappe.get_all( + 'Item Variant Attribute', + {'variant_of': parent_item_code}, + ['parent', 'attribute', 'attribute_value'], order_by='name', as_list=1 ) - unpublished_items = set([i.item_code for i in frappe.db.get_all('Website Item', filters={'published': 0}, fields=["item_code"])]) + disabled_items = set( + [i.name for i in frappe.db.get_all('Item', {'disabled': 1})] + ) - attribute_value_item_map = frappe._dict({}) - item_attribute_value_map = frappe._dict({}) + attribute_value_item_map = frappe._dict() + item_attribute_value_map = frappe._dict() - # dont consider variants that are unpublished - # (either have no Website Item or are unpublished in Website Item) - item_variants_data = [r for r in item_variants_data if r[0] not in unpublished_items] - item_variants_data = [r for r in item_variants_data if frappe.db.exists("Website Item", {"item_code": r[0]})] + # dont consider variants that are disabled + # pull all other variants + item_variants_data = [r for r in item_variants_data if r[0] not in disabled_items] for row in item_variants_data: item_code, attribute, attribute_value = row diff --git a/erpnext/e_commerce/variant_selector/test_variant_selector.py b/erpnext/e_commerce/variant_selector/test_variant_selector.py index c70fee5fff5..31c86f1fd53 100644 --- a/erpnext/e_commerce/variant_selector/test_variant_selector.py +++ b/erpnext/e_commerce/variant_selector/test_variant_selector.py @@ -1,11 +1,96 @@ -# import frappe import unittest -# from erpnext.e_commerce.product_data_engine.query import ProductQuery -# from erpnext.e_commerce.doctype.website_item.website_item import make_website_item +import frappe + +from erpnext.controllers.item_variant import create_variant +from erpnext.e_commerce.doctype.website_item.website_item import make_website_item +from erpnext.stock.doctype.item.test_item import make_item test_dependencies = ["Item"] class TestVariantSelector(unittest.TestCase): - # TODO: Variant Selector Tests - pass \ No newline at end of file + + def setUp(self) -> None: + self.template_item = make_item("Test-Tshirt-Temp", { + "has_variant": 1, + "variant_based_on": "Item Attribute", + "attributes": [ + { + "attribute": "Test Size" + }, + { + "attribute": "Test Colour" + } + ] + }) + + # create L-R, L-G, M-R, M-G and S-R + for size in ("Large", "Medium",): + for colour in ("Red", "Green",): + variant = create_variant("Test-Tshirt-Temp", { + "Test Size": size, + "Test Colour": colour + }) + variant.save() + + variant = create_variant("Test-Tshirt-Temp", { + "Test Size": "Small", + "Test Colour": "Red" + }) + variant.save() + + def tearDown(self): + frappe.db.rollback() + + def test_item_attributes(self): + """ + Test if the right attributes are fetched in the popup. + (Attributes must only come from active items) + + Attribute selection must not be linked to Website Items. + """ + from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values + + make_website_item(self.template_item) # publish template not variants + + attr_data = get_attributes_and_values("Test-Tshirt-Temp") + + self.assertEqual(attr_data[0]["attribute"], "Test Size") + self.assertEqual(attr_data[1]["attribute"], "Test Colour") + self.assertEqual(len(attr_data[0]["values"]), 3) # ['Small', 'Medium', 'Large'] + self.assertEqual(len(attr_data[1]["values"]), 2) # ['Red', 'Green'] + + # disable small red tshirt, now there are no small tshirts. + # but there are some red tshirts + small_variant = frappe.get_doc("Item", "Test-Tshirt-Temp-S-R") + small_variant.disabled = 1 + small_variant.save() # trigger cache rebuild + + attr_data = get_attributes_and_values("Test-Tshirt-Temp") + + # Only L and M attribute values must be fetched since S is disabled + self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large'] + + # teardown + small_variant.disabled = 1 + small_variant.save() + + def test_next_item_variant_values(self): + """ + Test if on selecting an attribute value, the next possible values + are filtered accordingly. + Values that dont apply should not be fetched. + E.g. + There is a ** Small-Red ** Tshirt. No other colour in this size. + On selecting ** Small **, only ** Red ** should be selectable next. + """ + from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values + + next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"}) + next_colours = next_values["valid_options_for_attributes"]["Test Colour"] + filtered_items = next_values["filtered_items"] + + self.assertEqual(len(next_colours), 1) + self.assertEqual(next_colours.pop(), "Red") + self.assertEqual(len(filtered_items), 1) + self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R") diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py index 89f146576bb..46d63003cce 100644 --- a/erpnext/healthcare/doctype/patient/patient.py +++ b/erpnext/healthcare/doctype/patient/patient.py @@ -143,7 +143,7 @@ class Patient(Document): age = self.age if not age: return - age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") + age_str = str(age.years) + ' ' + _("Year(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)") return age_str @frappe.whitelist() diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py index 7dcfac249f4..b1eaaf8b587 100644 --- a/erpnext/hr/doctype/attendance/attendance.py +++ b/erpnext/hr/doctype/attendance/attendance.py @@ -5,9 +5,9 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import cstr, formatdate, get_datetime, getdate, nowdate +from frappe.utils import cint, cstr, formatdate, get_datetime, getdate, nowdate -from erpnext.hr.utils import validate_active_employee +from erpnext.hr.utils import get_holiday_dates_for_employee, validate_active_employee class Attendance(Document): @@ -171,7 +171,7 @@ def get_month_map(): }) @frappe.whitelist() -def get_unmarked_days(employee, month): +def get_unmarked_days(employee, month, exclude_holidays=0): import calendar month_map = get_month_map() @@ -191,6 +191,11 @@ def get_unmarked_days(employee, month): ]) marked_days = [get_datetime(record.attendance_date) for record in records] + if cint(exclude_holidays): + holiday_dates = get_holiday_dates_for_employee(employee, month_start, month_end) + holidays = [get_datetime(record) for record in holiday_dates] + marked_days.extend(holidays) + unmarked_days = [] for date in dates_of_month: diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js index 6b3c29a76b4..3a5c5915396 100644 --- a/erpnext/hr/doctype/attendance/attendance_list.js +++ b/erpnext/hr/doctype/attendance/attendance_list.js @@ -28,6 +28,7 @@ frappe.listview_settings['Attendance'] = { onchange: function() { dialog.set_df_property("unmarked_days", "hidden", 1); dialog.set_df_property("status", "hidden", 1); + dialog.set_df_property("exclude_holidays", "hidden", 1); dialog.set_df_property("month", "value", ''); dialog.set_df_property("unmarked_days", "options", []); dialog.no_unmarked_days_left = false; @@ -42,9 +43,14 @@ frappe.listview_settings['Attendance'] = { onchange: function() { if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { dialog.set_df_property("status", "hidden", 0); + dialog.set_df_property("exclude_holidays", "hidden", 0); dialog.set_df_property("unmarked_days", "options", []); dialog.no_unmarked_days_left = false; - me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options => { + me.get_multi_select_options( + dialog.fields_dict.employee.value, + dialog.fields_dict.month.value, + dialog.fields_dict.exclude_holidays.get_value() + ).then(options => { if (options.length > 0) { dialog.set_df_property("unmarked_days", "hidden", 0); dialog.set_df_property("unmarked_days", "options", options); @@ -64,6 +70,31 @@ frappe.listview_settings['Attendance'] = { reqd: 1, }, + { + label: __("Exclude Holidays"), + fieldtype: "Check", + fieldname: "exclude_holidays", + hidden: 1, + onchange: function() { + if (dialog.fields_dict.employee.value && dialog.fields_dict.month.value) { + dialog.set_df_property("status", "hidden", 0); + dialog.set_df_property("unmarked_days", "options", []); + dialog.no_unmarked_days_left = false; + me.get_multi_select_options( + dialog.fields_dict.employee.value, + dialog.fields_dict.month.value, + dialog.fields_dict.exclude_holidays.get_value() + ).then(options => { + if (options.length > 0) { + dialog.set_df_property("unmarked_days", "hidden", 0); + dialog.set_df_property("unmarked_days", "options", options); + } else { + dialog.no_unmarked_days_left = true; + } + }); + } + } + }, { label: __("Unmarked Attendance for days"), fieldname: "unmarked_days", @@ -105,7 +136,7 @@ frappe.listview_settings['Attendance'] = { }); }, - get_multi_select_options: function(employee, month) { + get_multi_select_options: function(employee, month, exclude_holidays) { return new Promise(resolve => { frappe.call({ method: 'erpnext.hr.doctype.attendance.attendance.get_unmarked_days', @@ -113,6 +144,7 @@ frappe.listview_settings['Attendance'] = { args: { employee: employee, month: month, + exclude_holidays: exclude_holidays } }).then(r => { var options = []; diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py index df6e9bde006..daa068e6e03 100644 --- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py +++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py @@ -17,7 +17,10 @@ class TestEmployeeOnboarding(unittest.TestCase): def test_employee_onboarding_incomplete_task(self): if frappe.db.exists('Employee Onboarding', {'employee_name': 'Test Researcher'}): frappe.delete_doc('Employee Onboarding', {'employee_name': 'Test Researcher'}) - _set_up() + frappe.db.sql("delete from `tabEmployee Onboarding`") + project = "Employee Onboarding : test@researcher.com" + frappe.db.sql("delete from tabProject where name=%s", project) + frappe.db.sql("delete from tabTask where project=%s", project) applicant = get_job_applicant() job_offer = create_job_offer(job_applicant=applicant.name) @@ -42,7 +45,7 @@ class TestEmployeeOnboarding(unittest.TestCase): onboarding.submit() project_name = frappe.db.get_value("Project", onboarding.project, "project_name") - self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com') + self.assertEqual(project_name, 'Employee Onboarding : test@researcher.com') # don't allow making employee if onboarding is not complete self.assertRaises(IncompleteTaskError, make_employee, onboarding.name) @@ -65,8 +68,8 @@ class TestEmployeeOnboarding(unittest.TestCase): self.assertEqual(employee.employee_name, 'Test Researcher') def get_job_applicant(): - if frappe.db.exists('Job Applicant', 'Test Researcher - test@researcher.com'): - return frappe.get_doc('Job Applicant', 'Test Researcher - test@researcher.com') + if frappe.db.exists('Job Applicant', 'test@researcher.com'): + return frappe.get_doc('Job Applicant', 'test@researcher.com') applicant = frappe.new_doc('Job Applicant') applicant.applicant_name = 'Test Researcher' applicant.email_id = 'test@researcher.com' diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json index 200f675221b..66b609cf990 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.json +++ b/erpnext/hr/doctype/job_applicant/job_applicant.json @@ -192,10 +192,11 @@ "idx": 1, "index_web_pages_for_search": 1, "links": [], - "modified": "2021-09-29 23:06:10.904260", + "modified": "2022-01-12 16:28:53.196881", "modified_by": "Administrator", "module": "HR", "name": "Job Applicant", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -210,10 +211,11 @@ "write": 1 } ], - "search_fields": "applicant_name", + "search_fields": "applicant_name, email_id, job_title, phone_number", "sender_field": "email_id", "sort_field": "modified", "sort_order": "ASC", + "states": [], "subject_field": "notes", "title_field": "applicant_name" } \ No newline at end of file diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.py b/erpnext/hr/doctype/job_applicant/job_applicant.py index f0b470b35e8..54ccfca38f7 100644 --- a/erpnext/hr/doctype/job_applicant/job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/job_applicant.py @@ -7,6 +7,7 @@ import frappe from frappe import _ from frappe.model.document import Document +from frappe.model.naming import append_number_if_name_exists from frappe.utils import validate_email_address from erpnext.hr.doctype.interview.interview import get_interviewers @@ -21,10 +22,11 @@ class JobApplicant(Document): self.get("__onload").job_offer = job_offer[0].name def autoname(self): - keys = filter(None, (self.applicant_name, self.email_id, self.job_title)) - if not keys: - frappe.throw(_("Name or Email is mandatory"), frappe.NameError) - self.name = " - ".join(keys) + self.name = self.email_id + + # applicant can apply more than once for a different job title or reapply + if frappe.db.exists("Job Applicant", self.name): + self.name = append_number_if_name_exists("Job Applicant", self.name) def validate(self): if self.email_id: diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py index 36dcf6b0740..bf1622028d8 100644 --- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py +++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py @@ -9,7 +9,26 @@ from erpnext.hr.doctype.designation.test_designation import create_designation class TestJobApplicant(unittest.TestCase): - pass + def test_job_applicant_naming(self): + applicant = frappe.get_doc({ + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com" + }).insert() + self.assertEqual(applicant.name, 'job_applicant_naming@example.com') + + applicant = frappe.get_doc({ + "doctype": "Job Applicant", + "status": "Open", + "applicant_name": "_Test Applicant", + "email_id": "job_applicant_naming@example.com" + }).insert() + self.assertEqual(applicant.name, 'job_applicant_naming@example.com-1') + + def tearDown(self): + frappe.db.rollback() + def create_job_applicant(**args): args = frappe._dict(args) diff --git a/erpnext/hr/doctype/leave_period/leave_period.json b/erpnext/hr/doctype/leave_period/leave_period.json index 9e895c34fb2..84ce1147e9a 100644 --- a/erpnext/hr/doctype/leave_period/leave_period.json +++ b/erpnext/hr/doctype/leave_period/leave_period.json @@ -1,294 +1,108 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "HR-LPR-.YYYY.-.#####", - "beta": 0, "creation": "2018-04-13 15:20:52.864288", - "custom": 0, - "docstatus": 0, "doctype": "DocType", - "document_type": "", "editable_grid": 1, "engine": "InnoDB", + "field_order": [ + "from_date", + "to_date", + "is_active", + "column_break_3", + "company", + "optional_holiday_list" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "from_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_standard_filter": 0, "label": "From Date", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "to_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_standard_filter": 0, "label": "To Date", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "is_active", "fieldtype": "Check", - "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": "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 + "label": "Is Active" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "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, - "translatable": 0, - "unique": 0 + "fieldtype": "Column Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "company", "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": "Company", - "length": 0, - "no_copy": 0, "options": "Company", - "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 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "optional_holiday_list", "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", - "length": 0, - "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 + "options": "Holiday List" } ], - "has_web_view": 0, - "hide_heading": 0, - "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", + "links": [], + "modified": "2022-01-13 13:28:12.951025", "modified_by": "Administrator", "module": "HR", "name": "Leave Period", - "name_case": "", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "System Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR Manager", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 }, { - "amend": 0, - "cancel": 0, "create": 1, "delete": 1, "email": 1, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, "role": "HR User", - "set_user_permissions": 0, "share": 1, - "submit": 0, "write": 1 } ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, + "search_fields": "from_date, to_date, company", "sort_field": "modified", "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + "states": [], + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json index 3373350e733..27f0540b247 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json @@ -113,10 +113,11 @@ ], "is_submittable": 1, "links": [], - "modified": "2021-03-01 17:54:01.014509", + "modified": "2022-01-13 13:37:11.218882", "modified_by": "Administrator", "module": "HR", "name": "Leave Policy Assignment", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -164,5 +165,7 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], + "title_field": "employee_name", "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py index a0e1d19420c..c79216a275d 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py @@ -129,6 +129,8 @@ class LeavePolicyAssignment(Document): monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated, leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding) new_leaves_allocated = monthly_earned_leave * months_passed + else: + new_leaves_allocated = 0 return new_leaves_allocated diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js index 8b954c46a10..6b75817cba9 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js +++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js @@ -48,7 +48,16 @@ frappe.listview_settings['Leave Policy Assignment'] = { if (cur_dialog.fields_dict.leave_period.value) { 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" diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py index b1861ad4d8e..8953a51e8bb 100644 --- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py +++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py @@ -4,6 +4,7 @@ import unittest import frappe +from frappe.utils import add_months, get_first_day, getdate from erpnext.hr.doctype.leave_application.test_leave_application import ( get_employee, @@ -17,9 +18,8 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( test_dependencies = ["Employee"] class TestLeavePolicyAssignment(unittest.TestCase): - def setUp(self): - for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: + for doctype in ["Leave Period", "Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec def test_grant_leaves(self): @@ -54,8 +54,8 @@ class TestLeavePolicyAssignment(unittest.TestCase): self.assertEqual(leave_alloc_doc.new_leaves_allocated, 10) self.assertEqual(leave_alloc_doc.leave_type, "_Test Leave Type") - self.assertEqual(leave_alloc_doc.from_date, leave_period.from_date) - self.assertEqual(leave_alloc_doc.to_date, leave_period.to_date) + self.assertEqual(getdate(leave_alloc_doc.from_date), getdate(leave_period.from_date)) + self.assertEqual(getdate(leave_alloc_doc.to_date), getdate(leave_period.to_date)) self.assertEqual(leave_alloc_doc.leave_policy, leave_policy.name) self.assertEqual(leave_alloc_doc.leave_policy_assignment, leave_policy_assignments[0]) @@ -101,6 +101,55 @@ class TestLeavePolicyAssignment(unittest.TestCase): # User are now allowed to grant leave self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 0) + def test_earned_leave_allocation(self): + leave_period = create_leave_period("Test Earned Leave Period") + employee = get_employee() + leave_type = create_earned_leave_type("Test Earned Leave") + + leave_policy = frappe.get_doc({ + "doctype": "Leave Policy", + "leave_policy_details": [{"leave_type": leave_type.name, "annual_allocation": 6}] + }).insert() + + data = { + "assignment_based_on": "Leave Period", + "leave_policy": leave_policy.name, + "leave_period": leave_period.name + } + leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) + + # leaves allocated should be 0 since it is an earned leave and allocation happens via scheduler based on set frequency + leaves_allocated = frappe.db.get_value("Leave Allocation", { + "leave_policy_assignment": leave_policy_assignments[0] + }, "total_leaves_allocated") + self.assertEqual(leaves_allocated, 0) + def tearDown(self): - for doctype in ["Leave Application", "Leave Allocation", "Leave Policy Assignment", "Leave Ledger Entry"]: - frappe.db.sql("delete from `tab{0}`".format(doctype)) #nosec + frappe.db.rollback() + + +def create_earned_leave_type(leave_type): + frappe.delete_doc_if_exists("Leave Type", leave_type, force=1) + + return frappe.get_doc(dict( + leave_type_name=leave_type, + doctype="Leave Type", + is_earned_leave=1, + earned_leave_frequency="Monthly", + rounding=0.5, + max_leaves_allowed=6 + )).insert() + + +def create_leave_period(name): + frappe.delete_doc_if_exists("Leave Period", name, force=1) + start_date = get_first_day(getdate()) + + return frappe.get_doc(dict( + name=name, + doctype="Leave Period", + from_date=start_date, + to_date=add_months(start_date, 12), + company="_Test Company", + is_active=1 + )).insert() \ No newline at end of file diff --git a/erpnext/hr/doctype/shift_type/shift_type.js b/erpnext/hr/doctype/shift_type/shift_type.js index ba53312bcef..7138e3bcf30 100644 --- a/erpnext/hr/doctype/shift_type/shift_type.js +++ b/erpnext/hr/doctype/shift_type/shift_type.js @@ -4,15 +4,32 @@ frappe.ui.form.on('Shift Type', { refresh: function(frm) { frm.add_custom_button( - 'Mark Attendance', - () => frm.call({ - doc: frm.doc, - method: 'process_auto_attendance', - freeze: true, - callback: () => { - frappe.msgprint(__("Attendance has been marked as per employee check-ins")); + __('Mark Attendance'), + () => { + if (!frm.doc.enable_auto_attendance) { + frm.scroll_to_field('enable_auto_attendance'); + frappe.throw(__('Please Enable Auto Attendance and complete the setup first.')); } - }) + + if (!frm.doc.process_attendance_after) { + frm.scroll_to_field('process_attendance_after'); + frappe.throw(__('Please set {0}.', [__('Process Attendance After').bold()])); + } + + if (!frm.doc.last_sync_of_checkin) { + frm.scroll_to_field('last_sync_of_checkin'); + frappe.throw(__('Please set {0}.', [__('Last Sync of Checkin').bold()])); + } + + frm.call({ + doc: frm.doc, + method: 'process_auto_attendance', + freeze: true, + callback: () => { + frappe.msgprint(__('Attendance has been marked as per employee check-ins')); + } + }); + } ); } }); diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json index 6479853246f..93ef2170420 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.json @@ -13,8 +13,10 @@ "column_break_3", "company", "posting_date", - "is_term_loan", "rate_of_interest", + "payroll_payable_account", + "is_term_loan", + "repay_from_salary", "payment_details_section", "due_date", "pending_principal_amount", @@ -243,15 +245,31 @@ "label": "Total Penalty Paid", "options": "Company:company:default_currency", "read_only": 1 + }, + { + "depends_on": "eval:doc.repay_from_salary", + "fieldname": "payroll_payable_account", + "fieldtype": "Link", + "label": "Payroll Payable Account", + "mandatory_depends_on": "eval:doc.repay_from_salary", + "options": "Account" + }, + { + "default": "0", + "fetch_from": "against_loan.repay_from_salary", + "fieldname": "repay_from_salary", + "fieldtype": "Check", + "label": "Repay From Salary" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2021-04-19 18:10:00.935364", + "modified": "2022-01-06 01:51:06.707782", "modified_by": "Administrator", "module": "Loan Management", "name": "Loan Repayment", + "naming_rule": "Expression (old style)", "owner": "Administrator", "permissions": [ { @@ -287,5 +305,6 @@ ], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py index ceaaecb93a0..65099da49b4 100644 --- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py +++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py @@ -321,74 +321,79 @@ class LoanRepayment(AccountsController): else: remarks = _("Repayment against Loan: ") + self.against_loan - if not loan_details.repay_from_salary: - if self.total_penalty_paid: - gle_map.append( - self.get_gl_dict({ - "account": loan_details.loan_account, - "against": loan_details.payment_account, - "debit": self.total_penalty_paid, - "debit_in_account_currency": self.total_penalty_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, - "cost_center": self.cost_center, - "party_type": self.applicant_type, - "party": self.applicant, - "posting_date": getdate(self.posting_date) - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": loan_details.penalty_income_account, - "against": loan_details.payment_account, - "credit": self.total_penalty_paid, - "credit_in_account_currency": self.total_penalty_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": _("Penalty against loan:") + self.against_loan, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) - - gle_map.append( - self.get_gl_dict({ - "account": loan_details.payment_account, - "against": loan_details.loan_account + ", " + loan_details.interest_income_account - + ", " + loan_details.penalty_income_account, - "debit": self.amount_paid, - "debit_in_account_currency": self.amount_paid, - "against_voucher_type": "Loan", - "against_voucher": self.against_loan, - "remarks": remarks, - "cost_center": self.cost_center, - "posting_date": getdate(self.posting_date) - }) - ) + if self.repay_from_salary: + payment_account = self.payroll_payable_account + else: + payment_account = loan_details.payment_account + if self.total_penalty_paid: gle_map.append( self.get_gl_dict({ "account": loan_details.loan_account, - "party_type": loan_details.applicant_type, - "party": loan_details.applicant, "against": loan_details.payment_account, - "credit": self.amount_paid, - "credit_in_account_currency": self.amount_paid, + "debit": self.total_penalty_paid, + "debit_in_account_currency": self.total_penalty_paid, "against_voucher_type": "Loan", "against_voucher": self.against_loan, - "remarks": remarks, + "remarks": _("Penalty against loan:") + self.against_loan, + "cost_center": self.cost_center, + "party_type": self.applicant_type, + "party": self.applicant, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.penalty_income_account, + "against": payment_account, + "credit": self.total_penalty_paid, + "credit_in_account_currency": self.total_penalty_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": _("Penalty against loan:") + self.against_loan, "cost_center": self.cost_center, "posting_date": getdate(self.posting_date) }) ) - if gle_map: - make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) + gle_map.append( + self.get_gl_dict({ + "account": payment_account, + "against": loan_details.loan_account + ", " + loan_details.interest_income_account + + ", " + loan_details.penalty_income_account, + "debit": self.amount_paid, + "debit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": remarks, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + + gle_map.append( + self.get_gl_dict({ + "account": loan_details.loan_account, + "party_type": loan_details.applicant_type, + "party": loan_details.applicant, + "against": payment_account, + "credit": self.amount_paid, + "credit_in_account_currency": self.amount_paid, + "against_voucher_type": "Loan", + "against_voucher": self.against_loan, + "remarks": remarks, + "cost_center": self.cost_center, + "posting_date": getdate(self.posting_date) + }) + ) + + if gle_map: + make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False) def create_repayment_entry(loan, applicant, company, posting_date, loan_type, - payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None): + payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None, + payroll_payable_account=None): lr = frappe.get_doc({ "doctype": "Loan Repayment", @@ -401,7 +406,8 @@ def create_repayment_entry(loan, applicant, company, posting_date, loan_type, "interest_payable": interest_payable, "payable_principal_amount": payable_principal_amount, "amount_paid": amount_paid, - "loan_type": loan_type + "loan_type": loan_type, + "payroll_payable_account": payroll_payable_account }).insert() return lr diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 0ac64c2cfca..f75038eab34 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -531,16 +531,6 @@ class BOM(WebsiteGenerator): row.hour_rate = (hour_rate / flt(self.conversion_rate) if self.conversion_rate and hour_rate else hour_rate) - if self.routing: - time_in_mins = flt(frappe.db.get_value("BOM Operation", { - "workstation": row.workstation, - "operation": row.operation, - "parent": self.routing - }, ["time_in_mins"])) - - if time_in_mins: - row.time_in_mins = time_in_mins - if row.hour_rate and row.time_in_mins: row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate) row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0 diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py index e90b0a7d6d2..8bd60ea4aca 100644 --- a/erpnext/manufacturing/doctype/routing/test_routing.py +++ b/erpnext/manufacturing/doctype/routing/test_routing.py @@ -46,6 +46,7 @@ class TestRouting(ERPNextTestCase): wo_doc.delete() def test_update_bom_operation_time(self): + """Update cost shouldn't update routing times.""" operations = [ { "operation": "Test Operation A", @@ -85,8 +86,8 @@ class TestRouting(ERPNextTestCase): routing_doc.save() bom_doc.update_cost() bom_doc.reload() - self.assertEqual(bom_doc.operations[0].time_in_mins, 90) - self.assertEqual(bom_doc.operations[1].time_in_mins, 42.2) + self.assertEqual(bom_doc.operations[0].time_in_mins, 30) + self.assertEqual(bom_doc.operations[1].time_in_mins, 20) def setup_operations(rows): diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py index aa19b2f1003..1c9cd0cbf4d 100644 --- a/erpnext/manufacturing/doctype/work_order/test_work_order.py +++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py @@ -2,7 +2,7 @@ # License: GNU General Public License v3. See license.txt import frappe -from frappe.utils import add_months, cint, flt, now, today +from frappe.utils import add_days, add_months, cint, flt, now, today from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom @@ -12,6 +12,7 @@ from erpnext.manufacturing.doctype.work_order.work_order import ( OverProductionError, StockOverProductionError, close_work_order, + make_job_card, make_stock_entry, stop_unstop, ) @@ -801,6 +802,34 @@ class TestWorkOrder(ERPNextTestCase): if row.is_scrap_item: self.assertEqual(row.qty, 1) + # Partial Job Card 1 with qty 10 + wo_order = make_wo_order_test_record(item=item, company=company, planned_start_date=add_days(now(), 60), qty=20, skip_transfer=1) + job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name}, 'name') + update_job_card(job_card, 10) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + for row in stock_entry.items: + if row.is_scrap_item: + self.assertEqual(row.qty, 2) + + # Partial Job Card 2 with qty 10 + operations = [] + wo_order.load_from_db() + for row in wo_order.operations: + n_dict = row.as_dict() + n_dict['qty'] = 10 + n_dict['pending_qty'] = 10 + operations.append(n_dict) + + make_job_card(wo_order.name, operations) + job_card = frappe.db.get_value('Job Card', {'work_order': wo_order.name, 'docstatus': 0}, 'name') + update_job_card(job_card, 10) + + stock_entry = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 10)) + for row in stock_entry.items: + if row.is_scrap_item: + self.assertEqual(row.qty, 2) + def test_close_work_order(self): items = ['Test FG Item for Closed WO', 'Test RM Item 1 for Closed WO', 'Test RM Item 2 for Closed WO'] @@ -841,7 +870,9 @@ class TestWorkOrder(ERPNextTestCase): close_work_order(wo_order, "Closed") self.assertEqual(wo_order.get('status'), "Closed") -def update_job_card(job_card): +def update_job_card(job_card, jc_qty=None): + employee = frappe.db.get_value('Employee', {'status': 'Active'}, 'name') + job_card_doc = frappe.get_doc('Job Card', job_card) job_card_doc.set('scrap_items', [ { @@ -854,15 +885,18 @@ def update_job_card(job_card): }, ]) + if jc_qty: + job_card_doc.for_quantity = jc_qty + job_card_doc.append('time_logs', { 'from_time': now(), + 'employee': employee, 'time_in_mins': 60, 'completed_qty': job_card_doc.for_quantity }) job_card_doc.submit() - def get_scrap_item_details(bom_no): scrap_items = {} for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item` diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index e47103599e9..3f2f39e73af 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -131,16 +131,14 @@ frappe.ui.form.on("Work Order", { erpnext.work_order.set_custom_buttons(frm); frm.set_intro(""); - if (frm.doc.docstatus === 0 && !frm.doc.__islocal) { + if (frm.doc.docstatus === 0 && !frm.is_new()) { frm.set_intro(__("Submit this Work Order for further processing.")); + } else { + frm.trigger("show_progress_for_items"); + frm.trigger("show_progress_for_operations"); } if (frm.doc.status != "Closed") { - if (frm.doc.docstatus===1) { - frm.trigger('show_progress_for_items'); - frm.trigger('show_progress_for_operations'); - } - if (frm.doc.docstatus === 1 && frm.doc.operations && frm.doc.operations.length) { diff --git a/erpnext/patches.txt b/erpnext/patches.txt index c69ac090f65..8c9b365fa7e 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -178,7 +178,6 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list erpnext.patches.v12_0.set_default_payroll_based_on erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign -erpnext.patches.v13_0.validate_options_for_data_field erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123 erpnext.patches.v12_0.fix_quotation_expired_status erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry @@ -306,6 +305,7 @@ erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record erpnext.patches.v13_0.remove_bad_selling_defaults +erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022 erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") @@ -331,6 +331,7 @@ erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting erpnext.patches.v13_0.requeue_failed_reposts erpnext.patches.v13_0.fetch_thumbnail_in_website_items erpnext.patches.v13_0.update_job_card_status +erpnext.patches.v13_0.enable_uoms erpnext.patches.v12_0.update_production_plan_status erpnext.patches.v13_0.item_naming_series_not_mandatory erpnext.patches.v13_0.update_category_in_ltds_certificate @@ -339,3 +340,4 @@ erpnext.patches.v13_0.rename_ksa_qr_field erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021 erpnext.patches.v13_0.update_tax_category_for_rcm erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template +erpnext.patches.v13_0.agriculture_deprecation_warning diff --git a/erpnext/patches/v13_0/agriculture_deprecation_warning.py b/erpnext/patches/v13_0/agriculture_deprecation_warning.py new file mode 100644 index 00000000000..09ccfb3ea47 --- /dev/null +++ b/erpnext/patches/v13_0/agriculture_deprecation_warning.py @@ -0,0 +1,10 @@ +import click + + +def execute(): + + click.secho( + "Agriculture Domain is moved to a separate app and will be removed from ERPNext in version-14.\n" + "When upgrading to ERPNext version-14, please install the app to continue using the Agriculture domain: https://github.com/frappe/agriculture", + fg="yellow", + ) diff --git a/erpnext/patches/v13_0/enable_uoms.py b/erpnext/patches/v13_0/enable_uoms.py new file mode 100644 index 00000000000..4d3f6376303 --- /dev/null +++ b/erpnext/patches/v13_0/enable_uoms.py @@ -0,0 +1,13 @@ +import frappe + + +def execute(): + frappe.reload_doc('setup', 'doctype', 'uom') + + uom = frappe.qb.DocType("UOM") + + (frappe.qb + .update(uom) + .set(uom.enabled, 1) + .where(uom.creation >= "2021-10-18") # date when this field was released + ).run() diff --git a/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py new file mode 100644 index 00000000000..4ec22e9d0e1 --- /dev/null +++ b/erpnext/patches/v13_0/trim_whitespace_from_serial_nos.py @@ -0,0 +1,67 @@ +import frappe + +from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + + +def execute(): + broken_sles = frappe.db.sql(""" + select name, serial_no + from `tabStock Ledger Entry` + where + is_cancelled = 0 + 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 + "% ", # trailing whitespace + "%\n %", # leading whitespace on newline + "% \n%", # trailing whitespace on newline + "\n", # just new line + ), + as_dict=True, + ) + + frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sles) + + if not broken_sles: + return + + broken_serial_nos = set() + + # patch SLEs + for sle in broken_sles: + serial_no_list = get_serial_nos(sle.serial_no) + correct_sr_no = "\n".join(serial_no_list) + + if correct_sr_no == sle.serial_no: + continue + + frappe.db.set_value("Stock Ledger Entry", sle.name, "serial_no", correct_sr_no, update_modified=False) + broken_serial_nos.update(serial_no_list) + + if not broken_serial_nos: + return + + # Patch serial No documents if they don't have purchase info + # Purchase info is used for fetching incoming rate + broken_sr_no_records = frappe.get_list("Serial No", + filters={ + "status":"Active", + "name": ("in", broken_serial_nos), + "purchase_document_type": ("is", "not set") + }, + pluck="name", + ) + + frappe.db.MAX_WRITES_PER_TRANSACTION += len(broken_sr_no_records) + + patch_savepoint = "serial_no_patch" + for serial_no in broken_sr_no_records: + try: + frappe.db.savepoint(patch_savepoint) + sn = frappe.get_doc("Serial No", serial_no) + sn.update_serial_no_reference() + sn.db_update() + except Exception: + frappe.db.rollback(save_point=patch_savepoint) diff --git a/erpnext/patches/v13_0/validate_options_for_data_field.py b/erpnext/patches/v13_0/validate_options_for_data_field.py deleted file mode 100644 index ad777b8586d..00000000000 --- a/erpnext/patches/v13_0/validate_options_for_data_field.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (c) 2021, Frappe and Contributors -# License: GNU General Public License v3. See license.txt - - -import frappe -from frappe.model import data_field_options - - -def execute(): - - for field in frappe.get_all('Custom Field', - fields = ['name'], - filters = { - 'fieldtype': 'Data', - 'options': ['!=', None] - }): - - if field not in data_field_options: - frappe.db.sql(""" - UPDATE - `tabCustom Field` - SET - options=NULL - WHERE - name=%s - """, (field)) diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 84c59a2c2b8..99dfc231c74 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -60,6 +60,8 @@ class PayrollEntry(Document): def on_cancel(self): frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` 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): """ @@ -350,23 +352,24 @@ class PayrollEntry(Document): currencies = [] multi_currency = 0 company_currency = erpnext.get_company_currency(self.company) + accounting_dimensions = get_accounting_dimensions() or [] exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(self.payment_account, je_payment_amount, company_currency, currencies) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": self.payment_account, "bank_account": self.bank_account, "credit_in_account_currency": flt(amount, precision), "exchange_rate": flt(exchange_rate), - }) + }, accounting_dimensions)) exchange_rate, amount = self.get_amount_and_exchange_rate_for_journal_entry(payroll_payable_account, je_payment_amount, company_currency, currencies) - accounts.append({ + accounts.append(self.update_accounting_dimensions({ "account": payroll_payable_account, "debit_in_account_currency": flt(amount, precision), "exchange_rate": flt(exchange_rate), "reference_type": self.doctype, "reference_name": self.name - }) + }, accounting_dimensions)) if len(currencies) > 1: multi_currency = 1 diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index efbc4681d7a..8ea791f620f 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -936,8 +936,11 @@ class SalarySlip(TransactionBase): def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount): future_recurring_additional_amount = 0 to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date') + # future month count excluding current - future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month) + from_date, to_date = getdate(self.start_date), getdate(to_date) + future_recurring_period = ((to_date.year - from_date.year) * 12) + (to_date.month - from_date.month) + if future_recurring_period > 0: future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month return future_recurring_additional_amount @@ -1036,7 +1039,8 @@ class SalarySlip(TransactionBase): data.update({"annual_taxable_earning": annual_taxable_earning}) tax_amount = 0 for slab in tax_slab.slabs: - if slab.condition and not self.eval_tax_slab_condition(slab.condition, data): + cond = cstr(slab.condition).strip() + if cond and not self.eval_tax_slab_condition(cond, data): continue if not slab.to_amount and annual_taxable_earning >= slab.from_amount: tax_amount += (annual_taxable_earning - slab.from_amount + 1) * slab.percent_deduction *.01 @@ -1142,15 +1146,17 @@ class SalarySlip(TransactionBase): }) def make_loan_repayment_entry(self): + payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry) for loan in self.loans: - repayment_entry = create_repayment_entry(loan.loan, self.employee, - self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, - loan.principal_amount, loan.total_payment) + if loan.total_payment: + repayment_entry = create_repayment_entry(loan.loan, self.employee, + self.company, self.posting_date, loan.loan_type, "Regular Payment", loan.interest_amount, + loan.principal_amount, loan.total_payment, payroll_payable_account=payroll_payable_account) - repayment_entry.save() - repayment_entry.submit() + repayment_entry.save() + repayment_entry.submit() - frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name) + frappe.db.set_value("Salary Slip Loan", loan.name, "loan_repayment_entry", repayment_entry.name) def cancel_loan_repayment_entry(self): for loan in self.loans: @@ -1384,3 +1390,11 @@ def get_salary_component_data(component): ], as_dict=1, ) + +def get_payroll_payable_account(company, payroll_entry): + if payroll_entry: + payroll_payable_account = frappe.db.get_value('Payroll Entry', payroll_entry, 'payroll_payable_account') + else: + payroll_payable_account = frappe.db.get_value('Company', company, 'default_payroll_payable_account') + + return payroll_payable_account \ No newline at end of file diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py index 6227863365b..0977561c9e6 100644 --- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py @@ -384,7 +384,7 @@ class TestSalarySlip(unittest.TestCase): make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR', payroll_period=payroll_period) - frappe.db.sql("delete from tabLoan") + frappe.db.sql("delete from tabLoan where applicant = 'test_loan_repayment_salary_slip@salary.com'") loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1)) loan.repay_from_salary = 1 loan.submit() diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index c8657b29da5..6d3f20f271a 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -105,7 +105,7 @@ class Task(NestedSet): frappe.throw(_("Completed On cannot be greater than Today")) def update_depends_on(self): - depends_on_tasks = self.depends_on_tasks or "" + depends_on_tasks = "" for d in self.depends_on: if d.task and d.task not in depends_on_tasks: depends_on_tasks += d.task + "," diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js index 864c0957d1e..2b80efd6e33 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -114,6 +114,8 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ if ((!item.qty) && me.frm.doc.is_return) { item.amount = flt(item.rate * -1, precision("amount", item)); + } else if ((!item.qty) && me.frm.doc.is_debit_note) { + item.amount = flt(item.rate, precision("amount", item)); } else { item.amount = flt(item.rate * item.qty, precision("amount", item)); } @@ -708,14 +710,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]); if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)) { - var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; if(this.frm.doc.party_account_currency == this.frm.doc.currency) { var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount), precision("grand_total")); } else { var total_amount_to_pay = flt( - (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total")) + (flt(base_grand_total, precision("base_grand_total")) - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount), precision("base_grand_total") ); @@ -746,14 +749,15 @@ erpnext.taxes_and_totals = erpnext.payments.extend({ }, set_total_amount_to_default_mop: function() { - var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + let grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total; + let base_grand_total = this.frm.doc.base_rounded_total || this.frm.doc.base_grand_total; if(this.frm.doc.party_account_currency == this.frm.doc.currency) { var total_amount_to_pay = flt((grand_total - this.frm.doc.total_advance - this.frm.doc.write_off_amount), precision("grand_total")); } else { var total_amount_to_pay = flt( - (flt(grand_total*this.frm.doc.conversion_rate, precision("grand_total")) + (flt(base_grand_total, precision("base_grand_total")) - this.frm.doc.total_advance - this.frm.doc.base_write_off_amount), precision("base_grand_total") ); diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js index 07a93010b51..66531412faf 100644 --- a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js +++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.js @@ -2,7 +2,13 @@ // For license information, please see license.txt frappe.ui.form.on('UAE VAT Settings', { - // refresh: function(frm) { - - // } + onload: function(frm) { + frm.set_query('account', 'uae_vat_accounts', function() { + return { + filters: { + 'company': frm.doc.company + } + }; + }); + } }); diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 5b260c252d1..2287714a008 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -217,7 +217,7 @@ def get_regional_address_details(party_details, doctype, company): if tax_template_by_category: party_details['taxes_and_charges'] = tax_template_by_category - return + return party_details if not party_details.place_of_supply: return party_details if not party_details.company_gstin: return party_details diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index ef2bdb67980..4b98978f130 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -53,7 +53,8 @@ frappe.query_reports["GSTR-1"] = { { "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") }, { "value": "CDNR-UNREG", "label": __("Credit/Debit Notes (Unregistered) - 9B") }, { "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" } diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index b095c7293dd..17b11073346 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -41,7 +41,8 @@ class Gstr1Report(object): port_code, shipping_bill_number, shipping_bill_date, - reason_for_issuing_document + reason_for_issuing_document, + company_gstin """ def run(self): @@ -63,6 +64,8 @@ class Gstr1Report(object): self.get_b2c_data() elif self.filters.get("type_of_business") == "Advances": self.get_advance_data() + elif self.filters.get("type_of_business") == "NIL Rated": + self.get_nil_rated_invoices() elif self.invoices: for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) @@ -92,6 +95,57 @@ class Gstr1Report(object): row= [key[0], key[1], value[0], value[1]] 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): b2cs_output = {} @@ -241,10 +295,11 @@ class Gstr1Report(object): def get_invoice_items(self): self.invoice_items = frappe._dict() self.item_tax_rate = frappe._dict() + self.nil_exempt_non_gst = {} items = frappe.db.sql(""" - select item_code, parent, taxable_value, base_net_amount, item_tax_rate - from `tab%s Item` + select item_code, parent, taxable_value, base_net_amount, item_tax_rate, is_nil_exempt, + is_non_gst from `tab%s Item` where parent in (%s) """ % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) @@ -261,6 +316,16 @@ class Gstr1Report(object): tax_rate_dict = self.item_tax_rate.setdefault(d.parent, {}).setdefault(d.item_code, []) 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): self.tax_details = frappe.db.sql(""" select @@ -323,21 +388,24 @@ class Gstr1Report(object): self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) def get_columns(self): - self.tax_columns = [ - { - "fieldname": "rate", - "label": "Rate", - "fieldtype": "Int", - "width": 60 - }, - { - "fieldname": "taxable_value", - "label": "Taxable Value", - "fieldtype": "Currency", - "width": 100 - } - ] self.other_columns = [] + self.tax_columns = [] + + if self.filters.get("type_of_business") != "NIL Rated": + self.tax_columns = [ + { + "fieldname": "rate", + "label": "Rate", + "fieldtype": "Int", + "width": 60 + }, + { + "fieldname": "taxable_value", + "label": "Taxable Value", + "fieldtype": "Currency", + "width": 100 + } + ] if self.filters.get("type_of_business") == "B2B": self.invoice_columns = [ @@ -706,6 +774,33 @@ class Gstr1Report(object): "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 @@ -769,6 +864,11 @@ def get_json(filters, report_name, data): out = get_advances_json(res, gstin) 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 { 'report_name': report_name, 'report_type': filters['type_of_business'], @@ -981,6 +1081,36 @@ def get_cdnr_unreg_json(res, gstin): 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): if row.get('gst_category') == 'SEZ': if row.get('export_type') == 'WPAY': @@ -1065,3 +1195,9 @@ def download_json_file(): frappe.response['filecontent'] = data['data'] frappe.response['content_type'] = 'application/json' 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 \ No newline at end of file diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py index b41b2b0428f..cc26bd7a57a 100644 --- a/erpnext/regional/report/ksa_vat/ksa_vat.py +++ b/erpnext/regional/report/ksa_vat/ksa_vat.py @@ -20,25 +20,35 @@ def get_columns(): "fieldname": "title", "label": _("Title"), "fieldtype": "Data", - "width": 300 + "width": 300, }, { "fieldname": "amount", "label": _("Amount (SAR)"), "fieldtype": "Currency", + "options": "currency", "width": 150, }, { "fieldname": "adjustment_amount", "label": _("Adjustment (SAR)"), "fieldtype": "Currency", + "options": "currency", "width": 150, }, { "fieldname": "vat_amount", "label": _("VAT Amount (SAR)"), "fieldtype": "Currency", + "options": "currency", "width": 150, + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Currency", + "width": 150, + "hidden": 1 } ] @@ -47,6 +57,8 @@ def get_data(filters): # Validate if vat settings exist company = filters.get('company') + company_currency = frappe.get_cached_value('Company', company, "default_currency") + if frappe.db.exists('KSA VAT Setting', company) is None: url = get_url_to_list('KSA VAT Setting') frappe.msgprint(_('Create KSA VAT Setting for this company').format(url)) @@ -55,7 +67,7 @@ def get_data(filters): ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company) # Sales Heading - append_data(data, 'VAT on Sales', '', '', '') + append_data(data, 'VAT on Sales', '', '', '', company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 @@ -67,7 +79,7 @@ def get_data(filters): # Adding results to data append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax) + total_taxable_adjustment_amount, total_tax, company_currency) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount @@ -75,13 +87,13 @@ def get_data(filters): # Sales Grand Total append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax) + grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) # Blank Line - append_data(data, '', '', '', '') + append_data(data, '', '', '', '', company_currency) # Purchase Heading - append_data(data, 'VAT on Purchases', '', '', '') + append_data(data, 'VAT on Purchases', '', '', '', company_currency) grand_total_taxable_amount = 0 grand_total_taxable_adjustment_amount = 0 @@ -93,7 +105,7 @@ def get_data(filters): # Adding results to data append_data(data, vat_setting.title, total_taxable_amount, - total_taxable_adjustment_amount, total_tax) + total_taxable_adjustment_amount, total_tax, company_currency) grand_total_taxable_amount += total_taxable_amount grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount @@ -101,7 +113,7 @@ def get_data(filters): # Purchase Grand Total append_data(data, 'Grand Total', grand_total_taxable_amount, - grand_total_taxable_adjustment_amount, grand_total_tax) + grand_total_taxable_adjustment_amount, grand_total_tax, company_currency) return data @@ -147,9 +159,10 @@ def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype): -def append_data(data, title, amount, adjustment_amount, vat_amount): +def append_data(data, title, amount, adjustment_amount, vat_amount, company_currency): """Returns data with appended value.""" - data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount}) + data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount, + "currency": company_currency}) def get_tax_amount(item_code, account_head, doctype, parent): if doctype == 'Sales Invoice': diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index e61a634aaee..ce74f6d0a58 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -643,7 +643,7 @@ erpnext.PointOfSale.Controller = class { message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) }) } else if (available_qty < qty_needed) { - frappe.show_alert({ + frappe.throw({ message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2}.', [bold_item_code, bold_warehouse, bold_available_qty]), indicator: 'orange' }); diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js index 496385248c4..a30bcd7cf6d 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -113,7 +113,7 @@ erpnext.PointOfSale.ItemSelector = class { `