diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py index 1bd42f5aba..0ebf0eb541 100644 --- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py +++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py @@ -61,8 +61,10 @@ class AccountingDimension(Document): def on_update(self): frappe.flags.accounting_dimensions = None -def make_dimension_in_accounting_doctypes(doc): - doclist = get_doctypes_with_dimensions() +def make_dimension_in_accounting_doctypes(doc, doclist=None): + if not doclist: + doclist = get_doctypes_with_dimensions() + doc_count = len(get_accounting_dimensions()) count = 0 @@ -82,13 +84,13 @@ def make_dimension_in_accounting_doctypes(doc): "owner": "Administrator" } - if doctype == "Budget": - add_dimension_to_budget_doctype(df, doc) - else: - meta = frappe.get_meta(doctype, cached=False) - fieldnames = [d.fieldname for d in meta.get("fields")] + meta = frappe.get_meta(doctype, cached=False) + fieldnames = [d.fieldname for d in meta.get("fields")] - if df['fieldname'] not in fieldnames: + if df['fieldname'] not in fieldnames: + if doctype == "Budget": + add_dimension_to_budget_doctype(df.copy(), doc) + else: create_custom_field(doctype, df) count += 1 @@ -178,15 +180,7 @@ def toggle_disabling(doc): frappe.clear_cache(doctype=doctype) def get_doctypes_with_dimensions(): - doclist = ["GL Entry", "Sales Invoice", "POS Invoice", "Purchase Invoice", "Payment Entry", "Asset", - "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", - "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", - "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", - "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", - "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", - "Subscription Plan"] - - return doclist + return frappe.get_hooks("accounting_dimension_doctypes") def get_accounting_dimensions(as_list=True): if frappe.flags.accounting_dimensions is None: diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py index 342f21b93a..03c3eb0ac0 100644 --- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py +++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py @@ -22,9 +22,10 @@ def validate_company(company): 'allow_account_creation_against_child_company']) if parent_company and (not allow_account_creation_against_child_company): - frappe.throw(_("""{0} is a child company. Please import accounts against parent company - or enable {1} in company master""").format(frappe.bold(company), - frappe.bold('Allow Account Creation Against Child Company')), title='Wrong Company') + msg = _("{} is a child company. ").format(frappe.bold(company)) + msg += _("Please import accounts against parent company or enable {} in company master.").format( + frappe.bold('Allow Account Creation Against Child Company')) + frappe.throw(msg, title=_('Wrong Company')) if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1): return False @@ -74,7 +75,9 @@ def generate_data_from_csv(file_doc, as_dict=False): if as_dict: data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) else: - if not row[1]: row[1] = row[0] + if not row[1]: + row[1] = row[0] + row[3] = row[2] data.append(row) # convert csv data @@ -96,7 +99,9 @@ def generate_data_from_excel(file_doc, extension, as_dict=False): if as_dict: data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) else: - if not row[1]: row[1] = row[0] + if not row[1]: + row[1] = row[0] + row[3] = row[2] data.append(row) return data @@ -147,7 +152,13 @@ def build_forest(data): from frappe import _ for row in data: - account_name, parent_account = row[0:2] + account_name, parent_account, account_number, parent_account_number = row[0:4] + if account_number: + account_name = "{} - {}".format(account_number, account_name) + if parent_account_number: + parent_account_number = cstr(parent_account_number).strip() + parent_account = "{} - {}".format(parent_account_number, parent_account) + if parent_account == account_name == child: return [parent_account] elif account_name == child: @@ -159,20 +170,23 @@ def build_forest(data): charts_map, paths = {}, [] - line_no = 3 + line_no = 2 error_messages = [] for i in data: - account_name, dummy, account_number, is_group, account_type, root_type = i + account_name, parent_account, account_number, parent_account_number, is_group, account_type, root_type = i if not account_name: error_messages.append("Row {0}: Please enter Account Name".format(line_no)) + if account_number: + account_number = cstr(account_number).strip() + account_name = "{} - {}".format(account_number, account_name) + charts_map[account_name] = {} if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group if account_type: charts_map[account_name]["account_type"] = account_type if root_type: charts_map[account_name]["root_type"] = root_type - if account_number: charts_map[account_name]["account_number"] = account_number path = return_parent(data, account_name)[::-1] paths.append(path) # List of path is created line_no += 1 @@ -221,7 +235,7 @@ def download_template(file_type, template_type): def get_template(template_type): - fields = ["Account Name", "Parent Account", "Account Number", "Is Group", "Account Type", "Root Type"] + fields = ["Account Name", "Parent Account", "Account Number", "Parent Account Number", "Is Group", "Account Type", "Root Type"] writer = UnicodeWriter() writer.writerow(fields) @@ -241,23 +255,23 @@ def get_template(template_type): def get_sample_template(writer): template = [ - ["Application Of Funds(Assets)", "", "", 1, "", "Asset"], - ["Sources Of Funds(Liabilities)", "", "", 1, "", "Liability"], - ["Equity", "", "", 1, "", "Equity"], - ["Expenses", "", "", 1, "", "Expense"], - ["Income", "", "", 1, "", "Income"], - ["Bank Accounts", "Application Of Funds(Assets)", "", 1, "Bank", "Asset"], - ["Cash In Hand", "Application Of Funds(Assets)", "", 1, "Cash", "Asset"], - ["Stock Assets", "Application Of Funds(Assets)", "", 1, "Stock", "Asset"], - ["Cost Of Goods Sold", "Expenses", "", 0, "Cost of Goods Sold", "Expense"], - ["Asset Depreciation", "Expenses", "", 0, "Depreciation", "Expense"], - ["Fixed Assets", "Application Of Funds(Assets)", "", 0, "Fixed Asset", "Asset"], - ["Accounts Payable", "Sources Of Funds(Liabilities)", "", 0, "Payable", "Liability"], - ["Accounts Receivable", "Application Of Funds(Assets)", "", 1, "Receivable", "Asset"], - ["Stock Expenses", "Expenses", "", 0, "Stock Adjustment", "Expense"], - ["Sample Bank", "Bank Accounts", "", 0, "Bank", "Asset"], - ["Cash", "Cash In Hand", "", 0, "Cash", "Asset"], - ["Stores", "Stock Assets", "", 0, "Stock", "Asset"], + ["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"], + ["Sources Of Funds(Liabilities)", "", "", "", 1, "", "Liability"], + ["Equity", "", "", "", 1, "", "Equity"], + ["Expenses", "", "", "", 1, "", "Expense"], + ["Income", "", "", "", 1, "", "Income"], + ["Bank Accounts", "Application Of Funds(Assets)", "", "", 1, "Bank", "Asset"], + ["Cash In Hand", "Application Of Funds(Assets)", "", "", 1, "Cash", "Asset"], + ["Stock Assets", "Application Of Funds(Assets)", "", "", 1, "Stock", "Asset"], + ["Cost Of Goods Sold", "Expenses", "", "", 0, "Cost of Goods Sold", "Expense"], + ["Asset Depreciation", "Expenses", "", "", 0, "Depreciation", "Expense"], + ["Fixed Assets", "Application Of Funds(Assets)", "", "", 0, "Fixed Asset", "Asset"], + ["Accounts Payable", "Sources Of Funds(Liabilities)", "", "", 0, "Payable", "Liability"], + ["Accounts Receivable", "Application Of Funds(Assets)", "", "", 1, "Receivable", "Asset"], + ["Stock Expenses", "Expenses", "", "", 0, "Stock Adjustment", "Expense"], + ["Sample Bank", "Bank Accounts", "", "", 0, "Bank", "Asset"], + ["Cash", "Cash In Hand", "", "", 0, "Cash", "Asset"], + ["Stores", "Stock Assets", "", "", 0, "Stock", "Asset"], ] for row in template: diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 05652642eb..f0b4e2976d 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -136,7 +136,7 @@ class PricingRule(Document): for d in self.items: max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount") if max_discount and flt(self.discount_percentage) > flt(max_discount): - throw(_("Max discount allowed for item: {0} is {1}%").format(self.item_code, max_discount)) + throw(_("Max discount allowed for item: {0} is {1}%").format(d.item_code, max_discount)) def validate_price_list_with_currency(self): if self.currency and self.for_price_list: diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 9599d4ed0c..4217711363 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -21,6 +21,7 @@ from erpnext.accounts.general_ledger import get_round_off_account_and_cost_cente from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points from erpnext.accounts.deferred_revenue import validate_service_stop_date +from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details from frappe.model.utils import get_fetch_values from frappe.contacts.doctype.address.address import get_address_display @@ -75,6 +76,8 @@ class SalesInvoice(SellingController): if not self.is_pos: self.so_dn_required() + + self.set_tax_withholding() self.validate_proj_cust() self.validate_pos_return() @@ -153,6 +156,32 @@ class SalesInvoice(SellingController): if cost_center_company != self.company: frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company))) + def set_tax_withholding(self): + tax_withholding_details = get_party_tax_withholding_details(self) + + if not tax_withholding_details: + return + + accounts = [] + tax_withholding_account = tax_withholding_details.get("account_head") + + for d in self.taxes: + if d.account_head == tax_withholding_account: + d.update(tax_withholding_details) + accounts.append(d.account_head) + + if not accounts or tax_withholding_account not in accounts: + self.append("taxes", tax_withholding_details) + + to_remove = [d for d in self.taxes + if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account] + + for d in to_remove: + self.remove(d) + + # calculate totals again after applying TDS + self.calculate_taxes_and_totals() + def before_save(self): set_account_for_mode_of_payment(self) diff --git a/erpnext/accounts/doctype/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js index ba98eb9b2a..1a9066470a 100644 --- a/erpnext/accounts/doctype/subscription/subscription.js +++ b/erpnext/accounts/doctype/subscription/subscription.js @@ -10,6 +10,14 @@ frappe.ui.form.on('Subscription', { } } }); + + frm.set_query('cost_center', function() { + return { + filters: { + company: frm.doc.company + } + }; + }); }, refresh: function(frm) { diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json index afb94fe9c9..e80df2ab88 100644 --- a/erpnext/accounts/doctype/subscription/subscription.json +++ b/erpnext/accounts/doctype/subscription/subscription.json @@ -7,9 +7,10 @@ "engine": "InnoDB", "field_order": [ "party_type", - "status", - "cb_1", "party", + "cb_1", + "company", + "status", "subscription_period", "start_date", "end_date", @@ -44,80 +45,107 @@ { "allow_on_submit": 1, "fieldname": "cb_1", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "status", "fieldtype": "Select", "label": "Status", + "no_copy": 1, "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "subscription_period", "fieldtype": "Section Break", - "label": "Subscription Period" + "label": "Subscription Period", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cancelation_date", "fieldtype": "Date", "label": "Cancelation Date", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "trial_period_start", "fieldtype": "Date", "label": "Trial Period Start Date", - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.trial_period_start", "fieldname": "trial_period_end", "fieldtype": "Date", "label": "Trial Period End Date", - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "column_break_11", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "current_invoice_start", "fieldtype": "Date", "label": "Current Invoice Start Date", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "current_invoice_end", "fieldtype": "Date", "label": "Current Invoice End Date", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "description": "Number of days that the subscriber has to pay invoices generated by this subscription", "fieldname": "days_until_due", "fieldtype": "Int", - "label": "Days Until Due" + "label": "Days Until Due", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "cancel_at_period_end", "fieldtype": "Check", - "label": "Cancel At End Of Period" + "label": "Cancel At End Of Period", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "fieldname": "generate_invoice_at_period_start", "fieldtype": "Check", - "label": "Generate Invoice At Beginning Of Period" + "label": "Generate Invoice At Beginning Of Period", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, "fieldname": "sb_4", "fieldtype": "Section Break", - "label": "Plans" + "label": "Plans", + "show_days": 1, + "show_seconds": 1 }, { "allow_on_submit": 1, @@ -125,62 +153,84 @@ "fieldtype": "Table", "label": "Plans", "options": "Subscription Plan Detail", - "reqd": 1 + "reqd": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)", "fieldname": "sb_1", "fieldtype": "Section Break", - "label": "Taxes" + "label": "Taxes", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "sb_2", "fieldtype": "Section Break", - "label": "Discounts" + "label": "Discounts", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "apply_additional_discount", "fieldtype": "Select", "label": "Apply Additional Discount On", - "options": "\nGrand Total\nNet Total" + "options": "\nGrand Total\nNet Total", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cb_2", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "additional_discount_percentage", "fieldtype": "Percent", - "label": "Additional DIscount Percentage" + "label": "Additional DIscount Percentage", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "additional_discount_amount", "fieldtype": "Currency", - "label": "Additional DIscount Amount" + "label": "Additional DIscount Amount", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.invoices", "fieldname": "sb_3", "fieldtype": "Section Break", - "label": "Invoices" + "label": "Invoices", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "invoices", "fieldtype": "Table", "label": "Invoices", - "options": "Subscription Invoice" + "options": "Subscription Invoice", + "show_days": 1, + "show_seconds": 1 }, { "collapsible": 1, "fieldname": "accounting_dimensions_section", "fieldtype": "Section Break", - "label": "Accounting Dimensions" + "label": "Accounting Dimensions", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "dimension_col_break", - "fieldtype": "Column Break" + "fieldtype": "Column Break", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party_type", @@ -188,7 +238,9 @@ "label": "Party Type", "options": "DocType", "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "party", @@ -197,21 +249,27 @@ "label": "Party", "options": "party_type", "reqd": 1, - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.party_type === 'Customer'", "fieldname": "sales_tax_template", "fieldtype": "Link", "label": "Sales Taxes and Charges Template", - "options": "Sales Taxes and Charges Template" + "options": "Sales Taxes and Charges Template", + "show_days": 1, + "show_seconds": 1 }, { "depends_on": "eval:doc.party_type === 'Supplier'", "fieldname": "purchase_tax_template", "fieldtype": "Link", "label": "Purchase Taxes and Charges Template", - "options": "Purchase Taxes and Charges Template" + "options": "Purchase Taxes and Charges Template", + "show_days": 1, + "show_seconds": 1 }, { "default": "0", @@ -219,36 +277,55 @@ "fieldname": "follow_calendar_months", "fieldtype": "Check", "label": "Follow Calendar Months", - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "default": "0", "description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date", "fieldname": "generate_new_invoices_past_due_date", "fieldtype": "Check", - "label": "Generate New Invoices Past Due Date" + "label": "Generate New Invoices Past Due Date", + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "end_date", "fieldtype": "Date", "label": "Subscription End Date", - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "start_date", "fieldtype": "Date", "label": "Subscription Start Date", - "set_only_once": 1 + "set_only_once": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "cost_center", "fieldtype": "Link", "label": "Cost Center", - "options": "Cost Center" + "options": "Cost Center", + "show_days": 1, + "show_seconds": 1 + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "show_days": 1, + "show_seconds": 1 } ], + "index_web_pages_for_search": 1, "links": [], - "modified": "2020-06-25 10:52:52.265105", + "modified": "2021-02-09 15:44:20.024789", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription", diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index e023b47cac..826044a407 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -1,3 +1,4 @@ + # -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt @@ -5,12 +6,13 @@ from __future__ import unicode_literals import frappe +import erpnext from frappe import _ from frappe.model.document import Document from frappe.utils.data import nowdate, getdate, cstr, cint, add_days, date_diff, get_last_day, add_to_date, flt from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions - +from erpnext import get_default_company class Subscription(Document): def before_insert(self): @@ -243,6 +245,7 @@ class Subscription(Document): self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) self.validate_end_date() self.validate_to_follow_calendar_months() + self.cost_center = erpnext.get_default_cost_center(self.get('company')) def validate_trial_period(self): """ @@ -304,6 +307,14 @@ class Subscription(Document): doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' invoice = frappe.new_doc(doctype) + + # For backward compatibility + # Earlier subscription didn't had any company field + company = self.get('company') or get_default_company() + if not company: + frappe.throw(_("Company is mandatory was generating invoice. Please set default company in Global Defaults")) + + invoice.company = company invoice.set_posting_time = 1 invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \ else self.current_invoice_end @@ -330,6 +341,7 @@ class Subscription(Document): # for that reason items_list = self.get_items_from_plans(self.plans, prorate) for item in items_list: + item['cost_center'] = self.cost_center invoice.append('items', item) # Taxes @@ -380,7 +392,8 @@ class Subscription(Document): Returns the `Item`s linked to `Subscription Plan` """ if prorate: - prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start) + prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start, + self.generate_invoice_at_period_start) items = [] party = self.party @@ -583,10 +596,13 @@ def get_calendar_months(billing_interval): return calendar_months -def get_prorata_factor(period_end, period_start): - diff = flt(date_diff(nowdate(), period_start) + 1) - plan_days = flt(date_diff(period_end, period_start) + 1) - prorate_factor = diff / plan_days +def get_prorata_factor(period_end, period_start, is_prepaid): + if is_prepaid: + prorate_factor = 1 + else: + diff = flt(date_diff(nowdate(), period_start) + 1) + plan_days = flt(date_diff(period_end, period_start) + 1) + prorate_factor = diff / plan_days return prorate_factor diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index c17fccdce0..7c58e9865f 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -321,7 +321,8 @@ class TestSubscription(unittest.TestCase): self.assertEqual( flt( - get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start), + get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start, + subscription.generate_invoice_at_period_start), 2), flt(prorate_factor, 2) ) @@ -561,9 +562,7 @@ class TestSubscription(unittest.TestCase): current_inv = subscription.get_current_invoice() self.assertEqual(current_inv.status, "Unpaid") - diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) - prorate_factor = flt(diff / plan_days) + prorate_factor = 1 self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2)) diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json index f54e887f26..8a0d1de94c 100644 --- a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json +++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json @@ -13,21 +13,28 @@ "fieldname": "document_type", "fieldtype": "Link", "label": "Document Type ", + "no_copy": 1, "options": "DocType", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 }, { "fieldname": "invoice", "fieldtype": "Dynamic Link", "in_list_view": 1, "label": "Invoice", + "no_copy": 1, "options": "document_type", - "read_only": 1 + "read_only": 1, + "show_days": 1, + "show_seconds": 1 } ], + "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2020-06-01 22:23:54.462718", + "modified": "2021-02-09 15:43:32.026233", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Invoice", diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index 32ad4cb03a..961bdb147f 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -12,37 +12,62 @@ from erpnext.accounts.utils import get_fiscal_year class TaxWithholdingCategory(Document): pass -def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None): +def get_party_details(inv): + party_type, party = '', '' + if inv.doctype == 'Sales Invoice': + party_type = 'Customer' + party = inv.customer + else: + party_type = 'Supplier' + party = inv.supplier + + return party_type, party + +def get_party_tax_withholding_details(inv, tax_withholding_category=None): pan_no = '' - suppliers = [] + parties = [] + party_type, party = get_party_details(inv) if not tax_withholding_category: - tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan']) + tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan']) if not tax_withholding_category: return + # if tax_withholding_category passed as an argument but not pan_no if not pan_no: - pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan') + pan_no = frappe.db.get_value(party_type, party, 'pan') # Get others suppliers with the same PAN No if pan_no: - suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})] + parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name') - if not suppliers: - suppliers.append(ref_doc.supplier) + if not parties: + parties.append(party) + + fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company) + tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company) - fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company) - tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company) if not tax_details: frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') - .format(tax_withholding_category, ref_doc.company)) + .format(tax_withholding_category, inv.company)) - tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company, - tax_details, fy, ref_doc.posting_date, pan_no) + if party_type == 'Customer' and not tax_details.cumulative_threshold: + # TCS is only chargeable on sum of invoiced value + frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.') + .format(tax_withholding_category, inv.company, party)) - tax_row = get_tax_row(tax_details, tds_amount) + tax_amount, tax_deducted = get_tax_amount( + party_type, parties, + inv, tax_details, + fiscal_year, pan_no + ) + + if party_type == 'Supplier': + tax_row = get_tax_row_for_tds(tax_details, tax_amount) + else: + tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) return tax_row @@ -69,147 +94,254 @@ def get_tax_withholding_rates(tax_withholding, fiscal_year): frappe.throw(_("No Tax Withholding data found for the current Fiscal Year.")) -def get_tax_row(tax_details, tds_amount): - - return { +def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted): + row = { "category": "Total", - "add_deduct_tax": "Deduct", "charge_type": "Actual", - "account_head": tax_details.account_head, + "tax_amount": tax_amount, "description": tax_details.description, - "tax_amount": tds_amount + "account_head": tax_details.account_head } -def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None): - fiscal_year, year_start_date, year_end_date = fiscal_year_details - tds_amount = 0 - tds_deducted = 0 + if tax_deducted: + # TCS already deducted on previous invoices + # So, TCS will be calculated by 'Previous Row Total' - def _get_tds(amount, rate): - if amount <= 0: - return 0 - - return amount * rate / 100 - - ldc_name = frappe.db.get_value('Lower Deduction Certificate', - { - 'pan_no': pan_no, - 'fiscal_year': fiscal_year - }, 'name') - ldc = '' - - if ldc_name: - ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name) - - entries = frappe.db.sql(""" - select voucher_no, credit - from `tabGL Entry` - where company = %s and - party in %s and fiscal_year=%s and credit > 0 - and is_opening = 'No' - """, (company, tuple(suppliers), fiscal_year), as_dict=1) - - vouchers = [d.voucher_no for d in entries] - advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company) - - tds_vouchers = vouchers + advance_vouchers - - if tds_vouchers: - tds_deducted = frappe.db.sql(""" - SELECT sum(credit) FROM `tabGL Entry` - WHERE - account=%s and fiscal_year=%s and credit > 0 - and voucher_no in ({0})""". format(','.join(['%s'] * len(tds_vouchers))), - ((tax_details.account_head, fiscal_year) + tuple(tds_vouchers))) - - tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0 - - if tds_deducted: - if ldc: - limit_consumed = frappe.db.get_value('Purchase Invoice', - { - 'supplier': ('in', suppliers), - 'apply_tds': 1, - 'docstatus': 1 - }, 'sum(net_total)') - - if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, - ldc.certificate_limit): - - tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) + taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head] + if taxes_excluding_tcs: + # chargeable amount is the total amount after other charges are applied + row.update({ + "charge_type": "On Previous Row Total", + "row_id": len(taxes_excluding_tcs), + "rate": tax_details.rate + }) else: - tds_amount = _get_tds(net_total, tax_details.rate) - else: - supplier_credit_amount = frappe.get_all('Purchase Invoice', - fields = ['sum(net_total)'], - filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1) + # if only TCS is to be charged, then net total is chargeable amount + row.update({ + "charge_type": "On Net Total", + "rate": tax_details.rate + }) - supplier_credit_amount = (supplier_credit_amount[0][0] - if supplier_credit_amount and supplier_credit_amount[0][0] else 0) + return row - jv_supplier_credit_amt = frappe.get_all('Journal Entry Account', - fields = ['sum(credit_in_account_currency)'], - filters = { - 'parent': ('in', vouchers), 'docstatus': 1, - 'party': ('in', suppliers), - 'reference_type': ('not in', ['Purchase Invoice']) - }, as_list=1) +def get_tax_row_for_tds(tax_details, tax_amount): + return { + "category": "Total", + "charge_type": "Actual", + "tax_amount": tax_amount, + "add_deduct_tax": "Deduct", + "description": tax_details.description, + "account_head": tax_details.account_head + } - supplier_credit_amount += (jv_supplier_credit_amt[0][0] - if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0) +def get_lower_deduction_certificate(fiscal_year, pan_no): + ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name') + if ldc_name: + return frappe.get_doc('Lower Deduction Certificate', ldc_name) - supplier_credit_amount += net_total +def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None): + fiscal_year = fiscal_year_details[0] - debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date) - supplier_credit_amount -= debit_note_amount + vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type) + advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type) + taxable_vouchers = vouchers + advance_vouchers - if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold) - or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)): + tax_deducted = 0 + if taxable_vouchers: + tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details) - if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total, - ldc.certificate_limit): - tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate, - tax_details) + tax_amount = 0 + posting_date = inv.posting_date + if party_type == 'Supplier': + ldc = get_lower_deduction_certificate(fiscal_year, pan_no) + if tax_deducted: + net_total = inv.net_total + if ldc: + tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total) else: - tds_amount = _get_tds(supplier_credit_amount, tax_details.rate) + tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 + else: + tax_amount = get_tds_amount( + ldc, parties, inv, tax_details, + fiscal_year_details, tax_deducted, vouchers + ) + + elif party_type == 'Customer': + if tax_deducted: + # if already TCS is charged, then amount will be calculated based on 'Previous Row Total' + tax_amount = 0 + else: + # if no TCS has been charged in FY, + # then chargeable value is "prev invoices + advances" value which cross the threshold + tax_amount = get_tcs_amount( + parties, inv, tax_details, + fiscal_year_details, vouchers, advance_vouchers + ) + + return tax_amount, tax_deducted + +def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'): + dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' + + filters = { + dr_or_cr: ['>', 0], + 'company': company, + 'party_type': party_type, + 'party': ['in', parties], + 'fiscal_year': fiscal_year, + 'is_opening': 'No', + 'is_cancelled': 0 + } + + return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""] + +def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'): + # for advance vouchers, debit and credit is reversed + dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit' + + filters = { + dr_or_cr: ['>', 0], + 'is_opening': 'No', + 'is_cancelled': 0, + 'party_type': party_type, + 'party': ['in', parties], + 'against_voucher': ['is', 'not set'] + } + + if fiscal_year: + filters['fiscal_year'] = fiscal_year + if company: + filters['company'] = company + if from_date and to_date: + filters['posting_date'] = ['between', (from_date, to_date)] + + return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] + +def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details): + # check if TDS / TCS account is already charged on taxable vouchers + filters = { + 'is_cancelled': 0, + 'credit': ['>', 0], + 'fiscal_year': fiscal_year, + 'account': tax_details.account_head, + 'voucher_no': ['in', taxable_vouchers], + } + field = "sum(credit)" + + return frappe.db.get_value('GL Entry', filters, field) or 0.0 + +def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers): + tds_amount = 0 + + supp_credit_amt = frappe.db.get_value('Purchase Invoice', { + 'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1 + }, 'sum(net_total)') or 0.0 + + supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { + 'parent': ('in', vouchers), 'docstatus': 1, + 'party': ('in', parties), 'reference_type': ('!=', 'Purchase Invoice') + }, 'sum(credit_in_account_currency)') or 0.0 + + supp_credit_amt += supp_jv_credit_amt + supp_credit_amt += inv.net_total + + debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company) + supp_credit_amt -= debit_note_amount + + threshold = tax_details.get('threshold', 0) + cumulative_threshold = tax_details.get('cumulative_threshold', 0) + + if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): + if ldc and is_valid_certificate( + ldc.valid_from, ldc.valid_upto, + inv.posting_date, tax_deducted, + inv.net_total, ldc.certificate_limit + ): + tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) + else: + tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0 return tds_amount -def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None): - condition = "fiscal_year=%s" % fiscal_year +def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers): + tcs_amount = 0 + fiscal_year, _, _ = fiscal_year_details + + # sum of debit entries made from sales invoices + invoiced_amt = frappe.db.get_value('GL Entry', { + 'is_cancelled': 0, + 'party': ['in', parties], + 'company': inv.company, + 'voucher_no': ['in', vouchers], + }, 'sum(debit)') or 0.0 + + # sum of credit entries made from PE / JV with unset 'against voucher' + advance_amt = frappe.db.get_value('GL Entry', { + 'is_cancelled': 0, + 'party': ['in', parties], + 'company': inv.company, + 'voucher_no': ['in', adv_vouchers], + }, 'sum(credit)') or 0.0 + + # sum of credit entries made from sales invoice + credit_note_amt = frappe.db.get_value('GL Entry', { + 'is_cancelled': 0, + 'credit': ['>', 0], + 'party': ['in', parties], + 'fiscal_year': fiscal_year, + 'company': inv.company, + 'voucher_type': 'Sales Invoice', + }, 'sum(credit)') or 0.0 + + cumulative_threshold = tax_details.get('cumulative_threshold', 0) + + current_invoice_total = get_invoice_total_without_tcs(inv, tax_details) + total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt + + if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)): + chargeable_amt = total_invoiced_amt - cumulative_threshold + tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0 + + return tcs_amount + +def get_invoice_total_without_tcs(inv, tax_details): + tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head] + tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0 + + return inv.grand_total - tcs_tax_row_amount + +def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total): + tds_amount = 0 + limit_consumed = frappe.db.get_value('Purchase Invoice', { + 'supplier': ('in', parties), + 'apply_tds': 1, + 'docstatus': 1 + }, 'sum(net_total)') + + if is_valid_certificate( + ldc.valid_from, ldc.valid_upto, + posting_date, limit_consumed, + net_total, ldc.certificate_limit + ): + tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) + + return tds_amount + +def get_debit_note_amount(suppliers, fiscal_year_details, company=None): + _, year_start_date, year_end_date = fiscal_year_details + + filters = { + 'supplier': ['in', suppliers], + 'is_return': 1, + 'docstatus': 1, + 'posting_date': ['between', (year_start_date, year_end_date)] + } + fields = ['abs(sum(net_total)) as net_total'] if company: - condition += "and company =%s" % (company) - if from_date and to_date: - condition += "and posting_date between %s and %s" % (from_date, to_date) + filters['company'] = company - ## Appending the same supplier again if length of suppliers list is 1 - ## since tuple of single element list contains None, For example ('Test Supplier 1', ) - ## and the below query fails - if len(suppliers) == 1: - suppliers.append(suppliers[0]) - - return frappe.db.sql_list(""" - select distinct voucher_no - from `tabGL Entry` - where party in %s and %s and debit > 0 - and is_opening = 'No' - """, (tuple(suppliers), condition)) or [] - -def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None): - condition = "and 1=1" - if company: - condition = " and company=%s " % company - - if len(suppliers) == 1: - suppliers.append(suppliers[0]) - - return flt(frappe.db.sql(""" - select abs(sum(net_total)) - from `tabPurchase Invoice` - where supplier in %s and is_return=1 and docstatus=1 - and posting_date between %s and %s %s - """, (tuple(suppliers), year_start_date, year_end_date, condition))) + return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0 def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): if current_amount < (certificate_limit - deducted_amount): @@ -227,4 +359,4 @@ def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, certificate_limit > deducted_amount): valid = True - return valid \ No newline at end of file + return valid diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index ef77674372..9ce8e3fe83 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -9,7 +9,7 @@ from frappe.utils import today from erpnext.accounts.utils import get_fiscal_year from erpnext.buying.doctype.supplier.test_supplier import create_supplier -test_dependencies = ["Supplier Group"] +test_dependencies = ["Supplier Group", "Customer Group"] class TestTaxWithholdingCategory(unittest.TestCase): @classmethod @@ -18,6 +18,9 @@ class TestTaxWithholdingCategory(unittest.TestCase): create_records() create_tax_with_holding_category() + def tearDown(self): + cancel_invoices() + def test_cumulative_threshold_tds(self): frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS") invoices = [] @@ -128,9 +131,59 @@ class TestTaxWithholdingCategory(unittest.TestCase): for d in invoices: d.cancel() + def test_cumulative_threshold_tcs(self): + frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") + invoices = [] + + # create invoices for lower than single threshold tax rate + for _ in range(2): + si = create_sales_invoice(customer = "Test TCS Customer") + si.submit() + invoices.append(si) + + # create another invoice whose total when added to previously created invoice, + # surpasses cumulative threshhold + si = create_sales_invoice(customer = "Test TCS Customer", rate=12000) + si.submit() + + # assert tax collection on total invoice amount created until now + tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) + self.assertEqual(tcs_charged, 200) + self.assertEqual(si.grand_total, 12200) + invoices.append(si) + + # TCS is already collected once, so going forward system will collect TCS on every invoice + si = create_sales_invoice(customer = "Test TCS Customer", rate=5000) + si.submit() + + tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) + self.assertEqual(tcs_charged, 500) + invoices.append(si) + + #delete invoices to avoid clashing + for d in invoices: + d.cancel() + +def cancel_invoices(): + purchase_invoices = frappe.get_all("Purchase Invoice", { + 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']], + 'docstatus': 1 + }, pluck="name") + + sales_invoices = frappe.get_all("Sales Invoice", { + 'customer': 'Test TCS Customer', + 'docstatus': 1 + }, pluck="name") + + for d in purchase_invoices: + frappe.get_doc('Purchase Invoice', d).cancel() + + for d in sales_invoices: + frappe.get_doc('Sales Invoice', d).cancel() + def create_purchase_invoice(**args): # return sales invoice doc object - item = frappe.get_doc('Item', {'item_name': 'TDS Item'}) + item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name") args = frappe._dict(args) pi = frappe.get_doc({ @@ -145,7 +198,7 @@ def create_purchase_invoice(**args): "taxes": [], "items": [{ 'doctype': 'Purchase Invoice Item', - 'item_code': item.name, + 'item_code': item, 'qty': args.qty or 1, 'rate': args.rate or 10000, 'cost_center': 'Main - _TC', @@ -156,6 +209,33 @@ def create_purchase_invoice(**args): pi.save() return pi +def create_sales_invoice(**args): + # return sales invoice doc object + item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name") + + args = frappe._dict(args) + si = frappe.get_doc({ + "doctype": "Sales Invoice", + "posting_date": today(), + "customer": args.customer, + "company": '_Test Company', + "taxes_and_charges": "", + "currency": "INR", + "debit_to": "Debtors - _TC", + "taxes": [], + "items": [{ + 'doctype': 'Sales Invoice Item', + 'item_code': item, + 'qty': args.qty or 1, + 'rate': args.rate or 10000, + 'cost_center': 'Main - _TC', + 'expense_account': 'Cost of Goods Sold - _TC' + }] + }) + + si.save() + return si + def create_records(): # create a new suppliers for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: @@ -168,7 +248,17 @@ def create_records(): "doctype": "Supplier", }).insert() - # create an item + for name in ['Test TCS Customer']: + if frappe.db.exists('Customer', name): + continue + + frappe.get_doc({ + "customer_group": "_Test Customer Group", + "customer_name": name, + "doctype": "Customer" + }).insert() + + # create item if not frappe.db.exists('Item', "TDS Item"): frappe.get_doc({ "doctype": "Item", @@ -178,7 +268,16 @@ def create_records(): "is_stock_item": 0, }).insert() - # create an account + if not frappe.db.exists('Item', "TCS Item"): + frappe.get_doc({ + "doctype": "Item", + "item_code": "TCS Item", + "item_name": "TCS Item", + "item_group": "All Item Groups", + "is_stock_item": 1 + }).insert() + + # create tds account if not frappe.db.exists("Account", "TDS - _TC"): frappe.get_doc({ 'doctype': 'Account', @@ -189,6 +288,17 @@ def create_records(): 'root_type': 'Asset' }).insert() + # create tcs account + if not frappe.db.exists("Account", "TCS - _TC"): + frappe.get_doc({ + 'doctype': 'Account', + 'company': '_Test Company', + 'account_name': 'TCS', + 'parent_account': 'Duties and Taxes - _TC', + 'report_type': 'Balance Sheet', + 'root_type': 'Liability' + }).insert() + def create_tax_with_holding_category(): fiscal_year = get_fiscal_year(today(), company="_Test Company")[0] @@ -210,6 +320,23 @@ def create_tax_with_holding_category(): }] }).insert() + if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"): + frappe.get_doc({ + "doctype": "Tax Withholding Category", + "name": "Cumulative Threshold TCS", + "category_name": "10% TCS", + "rates": [{ + 'fiscal_year': fiscal_year, + 'tax_withholding_rate': 10, + 'single_threshold': 0, + 'cumulative_threshold': 30000.00 + }], + "accounts": [{ + 'company': '_Test Company', + 'account': 'TCS - _TC' + }] + }).insert() + # Single thresold if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"): frappe.get_doc({ diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index b42c0c61d9..dac0c216c8 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -196,7 +196,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): if not round_off_gle: for k in ["voucher_type", "voucher_no", "company", - "posting_date", "remarks", "is_opening"]: + "posting_date", "remarks"]: round_off_gle[k] = gl_map[0][k] round_off_gle.update({ @@ -208,6 +208,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): "cost_center": round_off_cost_center, "party_type": None, "party": None, + "is_opening": "No", "against_voucher_type": None, "against_voucher": None }) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 5eb2aab393..89a05b187d 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -897,17 +897,18 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa frappe.db.sql("""delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) - if not warehouse_account: warehouse_account = get_warehouse_account_map(company) + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2 + gle = get_voucherwise_gl_entries(stock_vouchers, posting_date) for voucher_type, voucher_no in stock_vouchers: existing_gle = gle.get((voucher_type, voucher_no), []) voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no) expected_gle = voucher_obj.get_gl_entries(warehouse_account) if expected_gle: - if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): + if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision): _delete_gl_entries(voucher_type, voucher_no) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) else: @@ -953,16 +954,17 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date): return gl_entries -def compare_existing_and_expected_gle(existing_gle, expected_gle): +def compare_existing_and_expected_gle(existing_gle, expected_gle, precision): matched = True for entry in expected_gle: account_existed = False for e in existing_gle: if entry.account == e.account: account_existed = True - if entry.account == e.account and entry.against_account == e.against_account \ - and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \ - and (entry.debit != e.debit or entry.credit != e.credit): + if (entry.account == e.account and entry.against_account == e.against_account + and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) + and ( flt(entry.debit, precision) != flt(e.debit, precision) or + flt(entry.credit, precision) != flt(e.credit, precision))): matched = False break if not account_existed: diff --git a/erpnext/assets/doctype/asset_category/asset_category.json b/erpnext/assets/doctype/asset_category/asset_category.json index b7d12269c6..a25f546903 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.json +++ b/erpnext/assets/doctype/asset_category/asset_category.json @@ -19,7 +19,6 @@ ], "fields": [ { - "depends_on": "eval:!doc.asset_category_name", "fieldname": "asset_category_name", "fieldtype": "Data", "in_list_view": 1, @@ -67,7 +66,7 @@ } ], "links": [], - "modified": "2021-01-22 12:31:14.425319", + "modified": "2021-02-24 15:05:38.621803", "modified_by": "Administrator", "module": "Assets", "name": "Asset Category", diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py index cc75a0afbe..148c1a6a16 100644 --- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py +++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py @@ -117,7 +117,7 @@ def call_mws_method(mws_method, *args, **kwargs): return response except Exception as e: delay = math.pow(4, x) * 125 - frappe.log_error(message=e, title=str(mws_method)) + frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed') time.sleep(delay) continue diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 39d3659b2b..59639ffc43 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -393,6 +393,15 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account communication_doctypes = ["Customer", "Supplier"] +accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset", + "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", + "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item", + "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule", + "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation", + "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription", + "Subscription Plan" +] + regional_overrides = { 'France': { 'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method' diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json index f99963504a..d8aae66796 100644 --- a/erpnext/hr/doctype/hr_settings/hr_settings.json +++ b/erpnext/hr/doctype/hr_settings/hr_settings.json @@ -138,7 +138,7 @@ "idx": 1, "issingle": 1, "links": [], - "modified": "2020-08-27 14:30:28.995324", + "modified": "2021-02-25 12:31:14.947865", "modified_by": "Administrator", "module": "HR", "name": "HR Settings", @@ -155,5 +155,6 @@ } ], "sort_field": "modified", - "sort_order": "ASC" -} + "sort_order": "ASC", + "track_changes": 1 +} \ No newline at end of file diff --git a/erpnext/hr/doctype/skill/skill.json b/erpnext/hr/doctype/skill/skill.json index 518297395b..a10381fac1 100644 --- a/erpnext/hr/doctype/skill/skill.json +++ b/erpnext/hr/doctype/skill/skill.json @@ -3,7 +3,7 @@ "allow_events_in_timeline": 0, "allow_guest_to_view": 0, "allow_import": 0, - "allow_rename": 0, + "allow_rename": 1, "autoname": "field:skill_name", "beta": 0, "creation": "2019-04-16 09:54:39.486915", @@ -56,7 +56,7 @@ "issingle": 0, "istable": 0, "max_attachments": 0, - "modified": "2019-04-16 09:55:00.536328", + "modified": "2021-02-24 09:55:00.536328", "modified_by": "Administrator", "module": "HR", "name": "Skill", @@ -110,4 +110,4 @@ "track_changes": 1, "track_seen": 0, "track_views": 0 -} \ No newline at end of file +} diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py index 06f7f989bb..3200363e01 100644 --- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py +++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py @@ -1,12 +1,13 @@ import frappe from frappe import _ +from frappe.utils import getdate, get_time from erpnext.stock.stock_ledger import update_entries_after from erpnext.accounts.utils import update_gl_entries_after def execute(): frappe.reload_doc('stock', 'doctype', 'repost_item_valuation') - reposting_project_deployed_on = frappe.db.get_value("DocType", "Repost Item Valuation", "creation") + reposting_project_deployed_on = get_creation_time() data = frappe.db.sql(''' SELECT @@ -40,7 +41,14 @@ def execute(): print("Reposting General Ledger Entries...") + posting_date = getdate(reposting_project_deployed_on) + posting_time = get_time(reposting_project_deployed_on) + for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): - update_gl_entries_after('2020-12-25', '01:58:55', company=row.name) + update_gl_entries_after(posting_date, posting_time, company=row.name) frappe.db.auto_commit_on_many_writes = 0 + +def get_creation_time(): + return frappe.db.sql(''' SELECT create_time FROM + INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0] \ No newline at end of file diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 3570a0f2be..077011ace0 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -75,24 +75,27 @@ frappe.ui.form.on("Project", { frm.add_custom_button(__('Cancelled'), () => { frm.events.set_status(frm, 'Cancelled'); }, __('Set Status')); - } - if (frappe.model.can_read("Task")) { - frm.add_custom_button(__("Gantt Chart"), function () { - frappe.route_options = { - "project": frm.doc.name - }; - frappe.set_route("List", "Task", "Gantt"); - }); - frm.add_custom_button(__("Kanban Board"), () => { - frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', { - project: frm.doc.project_name - }).then(() => { - frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name); + if (frappe.model.can_read("Task")) { + frm.add_custom_button(__("Gantt Chart"), function () { + frappe.route_options = { + "project": frm.doc.name + }; + frappe.set_route("List", "Task", "Gantt"); }); - }); + + frm.add_custom_button(__("Kanban Board"), () => { + frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', { + project: frm.doc.project_name + }).then(() => { + frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name); + }); + }); + } } + + }, create_duplicate: function(frm) { @@ -135,4 +138,4 @@ function open_form(frm, doctype, child_doctype, parentfield) { frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); }); -} \ No newline at end of file +} diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py index d85c82612a..62905385a3 100644 --- a/erpnext/projects/doctype/project/test_project.py +++ b/erpnext/projects/doctype/project/test_project.py @@ -37,7 +37,7 @@ class TestProject(unittest.TestCase): task1 = task_exists("Test Template Task Parent") if not task1: - task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1) + task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=4) task2 = task_exists("Test Template Task Child 1") if not task2: @@ -52,7 +52,7 @@ class TestProject(unittest.TestCase): tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc') self.assertEqual(tasks[0].subject, 'Test Template Task Parent') - self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1)) + self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 4)) self.assertEqual(tasks[1].subject, 'Test Template Task Child 1') self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3)) diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json index 69530b15b4..16caaa20ae 100644 --- a/erpnext/projects/doctype/project_template_task/project_template_task.json +++ b/erpnext/projects/doctype/project_template_task/project_template_task.json @@ -20,6 +20,7 @@ }, { "columns": 6, + "fetch_from": "task.subject", "fieldname": "subject", "fieldtype": "Read Only", "in_list_view": 1, @@ -28,7 +29,7 @@ ], "istable": 1, "links": [], - "modified": "2021-01-07 15:13:40.995071", + "modified": "2021-02-24 15:18:49.095071", "modified_by": "Administrator", "module": "Projects", "name": "Project Template Task", diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 16368aeddb..855ff5f83e 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -30,6 +30,7 @@ class Task(NestedSet): def validate(self): self.validate_dates() + self.validate_parent_expected_end_date() self.validate_parent_project_dates() self.validate_progress() self.validate_status() @@ -45,6 +46,12 @@ class Task(NestedSet): frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \ frappe.bold("Actual End Date"))) + def validate_parent_expected_end_date(self): + if self.parent_task: + parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date") + if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date): + frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date))) + def validate_parent_project_dates(self): if not self.project or frappe.flags.in_test: return diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index c39609bd38..e5bd4d7e05 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -513,6 +513,7 @@ erpnext.utils.update_child_items = function(opts) { }, { fieldtype:'Currency', fieldname:"rate", + options: "currency", default: 0, read_only: 0, in_list_view: 1, diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 322fa183f4..eea85cd2d6 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -10,6 +10,7 @@ import sys import json import base64 import frappe +import six import traceback import io from frappe import _, bold @@ -108,11 +109,13 @@ def get_party_details(address_name): pincode = 999999 return frappe._dict(dict( - gstin=d.gstin, legal_name=d.address_title, - location=d.city, pincode=d.pincode, + gstin=d.gstin, + legal_name=sanitize_for_json(d.address_title), + location=sanitize_for_json(d.city), + pincode=d.pincode, state_code=d.gst_state_number, - address_line1=d.address_line1, - address_line2=d.address_line2 + address_line1=sanitize_for_json(d.address_line1), + address_line2=sanitize_for_json(d.address_line2) )) def get_gstin_details(gstin): @@ -146,8 +149,11 @@ def get_overseas_address_details(address_name): ) return frappe._dict(dict( - gstin='URP', legal_name=address_title, location=city, - address_line1=address_line1, address_line2=address_line2, + gstin='URP', + legal_name=sanitize_for_json(address_title), + location=city, + address_line1=sanitize_for_json(address_line1), + address_line2=sanitize_for_json(address_line2), pincode=999999, state_code=96, place_of_supply=96 )) @@ -160,7 +166,7 @@ def get_item_list(invoice): item.update(d.as_dict()) item.sr_no = d.idx - item.description = json.dumps(d.item_name)[1:-1] + item.description = sanitize_for_json(d.item_name) item.qty = abs(item.qty) item.discount_amount = 0 @@ -326,7 +332,7 @@ def make_einvoice(invoice): buyer_details = get_overseas_address_details(invoice.customer_address) else: buyer_details = get_party_details(invoice.customer_address) - place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin + place_of_supply = get_place_of_supply(invoice, invoice.doctype) or sanitize_for_json(invoice.billing_address_gstin) place_of_supply = place_of_supply[:2] buyer_details.update(dict(place_of_supply=place_of_supply)) @@ -356,7 +362,7 @@ def make_einvoice(invoice): period_details=period_details, prev_doc_details=prev_doc_details, export_details=export_details, eway_bill_details=eway_bill_details ) - einvoice = json.loads(einvoice) + einvoice = safe_json_load(einvoice) validations = json.loads(read_json('einv_validation')) errors = validate_einvoice(validations, einvoice) @@ -371,6 +377,18 @@ def make_einvoice(invoice): return einvoice +def safe_json_load(json_string): + JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError + + try: + return json.loads(json_string) + except JSONDecodeError as e: + # print a snippet of 40 characters around the location where error occured + pos = e.pos + start, end = max(0, pos-20), min(len(json_string)-1, pos+20) + snippet = json_string[start:end] + frappe.throw(_("Error in input data. Please check for any special characters near following input:
{}").format(snippet)) + def validate_einvoice(validations, einvoice, errors=[]): for fieldname, field_validation in validations.items(): value = einvoice.get(fieldname, None) @@ -798,6 +816,13 @@ class GSPConnector(): self.invoice.flags.ignore_validate = True self.invoice.save() + +def sanitize_for_json(string): + """Escape JSON specific characters from a string.""" + + # json.dumps adds double-quotes to the string. Indexing to remove them. + return json.dumps(string)[1:-1] + @frappe.whitelist() def get_einvoice(doctype, docname): invoice = frappe.get_doc(doctype, docname) diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json index 3c0652eb67..7d5e84df52 100644 --- a/erpnext/selling/doctype/customer/customer.json +++ b/erpnext/selling/doctype/customer/customer.json @@ -16,6 +16,8 @@ "customer_name", "gender", "customer_type", + "pan", + "tax_withholding_category", "default_bank_account", "lead_name", "image", @@ -479,13 +481,25 @@ "fieldname": "dn_required", "fieldtype": "Check", "label": "Allow Sales Invoice Creation Without Delivery Note" + }, + { + "fieldname": "pan", + "fieldtype": "Data", + "label": "PAN" + }, + { + "fieldname": "tax_withholding_category", + "fieldtype": "Link", + "label": "Tax Withholding Category", + "options": "Tax Withholding Category" } ], "icon": "fa fa-user", "idx": 363, "image_field": "image", + "index_web_pages_for_search": 1, "links": [], - "modified": "2021-01-06 19:35:25.418017", + "modified": "2021-01-28 12:54:57.258959", "modified_by": "Administrator", "module": "Selling", "name": "Customer", diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js index 338a3ccf24..74b4bb085b 100644 --- a/erpnext/selling/page/point_of_sale/pos_controller.js +++ b/erpnext/selling/page/point_of_sale/pos_controller.js @@ -498,10 +498,11 @@ erpnext.PointOfSale.Controller = class { async on_cart_update(args) { frappe.dom.freeze(); + let item_row = undefined; try { let { field, value, item } = args; const { item_code, batch_no, serial_no, uom } = item; - let item_row = this.get_item_from_frm(item_code, batch_no, uom); + item_row = this.get_item_from_frm(item_code, batch_no, uom); const item_selected_from_selector = field === 'qty' && value === "+1" @@ -553,10 +554,12 @@ erpnext.PointOfSale.Controller = class { this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row); this.update_cart_html(item_row); } + } catch (error) { console.log(error); } finally { frappe.dom.unfreeze(); + return item_row; } } diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js index 044e80357d..9ab9eefa30 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_cart.js +++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js @@ -472,7 +472,8 @@ erpnext.PointOfSale.ItemCart = class { if (!frm) frm = this.events.get_frm(); this.render_net_total(frm.doc.net_total); - this.render_grand_total(frm.doc.grand_total); + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total; + this.render_grand_total(grand_total); const taxes = frm.doc.taxes.map(t => { return { 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 7c116e9fa1..e0d5b73166 100644 --- a/erpnext/selling/page/point_of_sale/pos_item_selector.js +++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js @@ -152,6 +152,10 @@ erpnext.PointOfSale.ItemSelector = class { this.item_group_field.toggle_label(false); } + set_search_value(value) { + $(this.search_field.$input[0]).val(value).trigger("input"); + } + bind_events() { const me = this; window.onScan = onScan; @@ -159,7 +163,7 @@ erpnext.PointOfSale.ItemSelector = class { onScan: (sScancode) => { if (this.search_field && this.$component.is(':visible')) { this.search_field.set_focus(); - $(this.search_field.$input[0]).val(sScancode).trigger("input"); + this.set_search_value(sScancode); this.barcode_scanned = true; } } @@ -178,6 +182,7 @@ erpnext.PointOfSale.ItemSelector = class { uom = uom === "undefined" ? undefined : uom; me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }}); + me.set_search_value(''); }); this.search_field.$input.on('input', (e) => { diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js index bcbac3b4be..22a279d463 100644 --- a/erpnext/selling/page/point_of_sale/pos_payment.js +++ b/erpnext/selling/page/point_of_sale/pos_payment.js @@ -223,7 +223,8 @@ erpnext.PointOfSale.Payment = class { if (success) { title = __("Payment Received"); - if (amount >= doc.grand_total) { + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; + if (amount >= grand_total) { frappe.dom.unfreeze(); message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]); this.events.submit_invoice(); @@ -243,7 +244,8 @@ erpnext.PointOfSale.Payment = class { auto_set_remaining_amount() { const doc = this.events.get_frm().doc; - const remaining_amount = doc.grand_total - doc.paid_amount; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; + const remaining_amount = grand_total - doc.paid_amount; const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined; if (!current_value && remaining_amount > 0 && this.selected_mode) { this.selected_mode.set_value(remaining_amount); @@ -389,7 +391,7 @@ erpnext.PointOfSale.Payment = class { } attach_cash_shortcuts(doc) { - const grand_total = doc.grand_total; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; const currency = doc.currency; const shortcuts = this.get_cash_shortcuts(flt(grand_total)); @@ -499,7 +501,8 @@ erpnext.PointOfSale.Payment = class { update_totals_section(doc) { if (!doc) doc = this.events.get_frm().doc; const paid_amount = doc.paid_amount; - const remaining = doc.grand_total - doc.paid_amount; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; + const remaining = grand_total - doc.paid_amount; const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined; const currency = doc.currency; const label = change ? __('Change') : __('To Be Paid'); @@ -507,7 +510,7 @@ erpnext.PointOfSale.Payment = class { this.$totals.html( `
Grand Total
-
${format_currency(doc.grand_total, currency)}
+
${format_currency(grand_total, currency)}
diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py index 9e240cc2b3..283f7d5fda 100644 --- a/erpnext/stock/__init__.py +++ b/erpnext/stock/__init__.py @@ -38,7 +38,7 @@ def get_warehouse_account_map(company=None): frappe.flags.warehouse_account_map[company] = warehouse_account else: frappe.flags.warehouse_account_map = warehouse_account - + return frappe.flags.warehouse_account_map.get(company) or frappe.flags.warehouse_account_map def get_warehouse_account(warehouse, warehouse_account=None): @@ -64,6 +64,10 @@ def get_warehouse_account(warehouse, warehouse_account=None): if not account and warehouse.company: account = get_company_default_inventory_account(warehouse.company) + if not account and warehouse.company: + account = frappe.db.get_value('Account', + {'account_type': 'Stock', 'is_group': 0, 'company': warehouse.company}, 'name') + if not account and warehouse.company and not warehouse.is_group: frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}") .format(warehouse.name, warehouse.company)) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index fcf7c2608e..6886c1ba45 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -521,8 +521,7 @@ "fieldname": "has_variants", "fieldtype": "Check", "in_standard_filter": 1, - "label": "Has Variants", - "no_copy": 1 + "label": "Has Variants" }, { "default": "Item Attribute", @@ -538,7 +537,6 @@ "fieldtype": "Table", "hidden": 1, "label": "Attributes", - "no_copy": 1, "options": "Item Variant Attribute" }, { @@ -1068,7 +1066,7 @@ "index_web_pages_for_search": 1, "links": [], "max_attachments": 1, - "modified": "2021-01-25 20:49:50.222976", + "modified": "2021-02-18 14:00:19.668049", "modified_by": "Administrator", "module": "Stock", "name": "Item", diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 550c849c5d..d72101412e 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -295,7 +295,8 @@ class PurchaseReceipt(BuyingController): "against": warehouse_account[d.warehouse]["account"], "cost_center": d.cost_center, "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount["base_amount"]), + "credit": (flt(amount["base_amount"]) if (amount["base_amount"] or + account_currency!=self.company_currency) else flt(amount["amount"])), "credit_in_account_currency": flt(amount["amount"]), "project": d.project }, item=d)) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 9cdc3cfa55..ea1b3873ea 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -276,9 +276,10 @@ class StockEntry(StockController): item_wise_qty.setdefault(d.item_code, []).append(d.qty) for item_code, qty_list in iteritems(item_wise_qty): - if self.fg_completed_qty != sum(qty_list): + total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty")) + if self.fg_completed_qty != total: frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different") - .format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty))) + .format(frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty))) def validate_difference_account(self): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js index 167e80fa39..9fe12f9490 100644 --- a/erpnext/support/doctype/issue/issue.js +++ b/erpnext/support/doctype/issue/issue.js @@ -49,8 +49,8 @@ frappe.ui.form.on("Issue", { }, refresh: function (frm) { - if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") { - if (frm.doc.service_level_agreement) { + if (frm.doc.status !== "Closed") { + if (frm.doc.service_level_agreement && frm.doc.agreement_status === "Ongoing") { frappe.call({ "method": "frappe.client.get", args: {