Merge branch 'develop' of https://github.com/frappe/erpnext into project_filter_search_fields

This commit is contained in:
Deepesh Garg 2021-02-27 17:29:11 +05:30
commit b5819c74c8
35 changed files with 787 additions and 297 deletions

View File

@ -61,8 +61,10 @@ class AccountingDimension(Document):
def on_update(self): def on_update(self):
frappe.flags.accounting_dimensions = None frappe.flags.accounting_dimensions = None
def make_dimension_in_accounting_doctypes(doc): def make_dimension_in_accounting_doctypes(doc, doclist=None):
doclist = get_doctypes_with_dimensions() if not doclist:
doclist = get_doctypes_with_dimensions()
doc_count = len(get_accounting_dimensions()) doc_count = len(get_accounting_dimensions())
count = 0 count = 0
@ -82,13 +84,13 @@ def make_dimension_in_accounting_doctypes(doc):
"owner": "Administrator" "owner": "Administrator"
} }
if doctype == "Budget": meta = frappe.get_meta(doctype, cached=False)
add_dimension_to_budget_doctype(df, doc) fieldnames = [d.fieldname for d in meta.get("fields")]
else:
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) create_custom_field(doctype, df)
count += 1 count += 1
@ -178,15 +180,7 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype) frappe.clear_cache(doctype=doctype)
def get_doctypes_with_dimensions(): def get_doctypes_with_dimensions():
doclist = ["GL Entry", "Sales Invoice", "POS Invoice", "Purchase Invoice", "Payment Entry", "Asset", return frappe.get_hooks("accounting_dimension_doctypes")
"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
def get_accounting_dimensions(as_list=True): def get_accounting_dimensions(as_list=True):
if frappe.flags.accounting_dimensions is None: if frappe.flags.accounting_dimensions is None:

View File

@ -22,9 +22,10 @@ def validate_company(company):
'allow_account_creation_against_child_company']) 'allow_account_creation_against_child_company'])
if parent_company and (not 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 msg = _("{} is a child company. ").format(frappe.bold(company))
or enable {1} in company master""").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')), title='Wrong Company') 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): if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1):
return False return False
@ -74,7 +75,9 @@ def generate_data_from_csv(file_doc, as_dict=False):
if as_dict: if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else: else:
if not row[1]: row[1] = row[0] if not row[1]:
row[1] = row[0]
row[3] = row[2]
data.append(row) data.append(row)
# convert csv data # convert csv data
@ -96,7 +99,9 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
if as_dict: if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)}) data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else: else:
if not row[1]: row[1] = row[0] if not row[1]:
row[1] = row[0]
row[3] = row[2]
data.append(row) data.append(row)
return data return data
@ -147,7 +152,13 @@ def build_forest(data):
from frappe import _ from frappe import _
for row in data: 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: if parent_account == account_name == child:
return [parent_account] return [parent_account]
elif account_name == child: elif account_name == child:
@ -159,20 +170,23 @@ def build_forest(data):
charts_map, paths = {}, [] charts_map, paths = {}, []
line_no = 3 line_no = 2
error_messages = [] error_messages = []
for i in data: 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: if not account_name:
error_messages.append("Row {0}: Please enter Account Name".format(line_no)) 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] = {} charts_map[account_name] = {}
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group 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 account_type: charts_map[account_name]["account_type"] = account_type
if root_type: charts_map[account_name]["root_type"] = root_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] path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created paths.append(path) # List of path is created
line_no += 1 line_no += 1
@ -221,7 +235,7 @@ def download_template(file_type, template_type):
def get_template(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 = UnicodeWriter()
writer.writerow(fields) writer.writerow(fields)
@ -241,23 +255,23 @@ def get_template(template_type):
def get_sample_template(writer): def get_sample_template(writer):
template = [ template = [
["Application Of Funds(Assets)", "", "", 1, "", "Asset"], ["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
["Sources Of Funds(Liabilities)", "", "", 1, "", "Liability"], ["Sources Of Funds(Liabilities)", "", "", "", 1, "", "Liability"],
["Equity", "", "", 1, "", "Equity"], ["Equity", "", "", "", 1, "", "Equity"],
["Expenses", "", "", 1, "", "Expense"], ["Expenses", "", "", "", 1, "", "Expense"],
["Income", "", "", 1, "", "Income"], ["Income", "", "", "", 1, "", "Income"],
["Bank Accounts", "Application Of Funds(Assets)", "", 1, "Bank", "Asset"], ["Bank Accounts", "Application Of Funds(Assets)", "", "", 1, "Bank", "Asset"],
["Cash In Hand", "Application Of Funds(Assets)", "", 1, "Cash", "Asset"], ["Cash In Hand", "Application Of Funds(Assets)", "", "", 1, "Cash", "Asset"],
["Stock Assets", "Application Of Funds(Assets)", "", 1, "Stock", "Asset"], ["Stock Assets", "Application Of Funds(Assets)", "", "", 1, "Stock", "Asset"],
["Cost Of Goods Sold", "Expenses", "", 0, "Cost of Goods Sold", "Expense"], ["Cost Of Goods Sold", "Expenses", "", "", 0, "Cost of Goods Sold", "Expense"],
["Asset Depreciation", "Expenses", "", 0, "Depreciation", "Expense"], ["Asset Depreciation", "Expenses", "", "", 0, "Depreciation", "Expense"],
["Fixed Assets", "Application Of Funds(Assets)", "", 0, "Fixed Asset", "Asset"], ["Fixed Assets", "Application Of Funds(Assets)", "", "", 0, "Fixed Asset", "Asset"],
["Accounts Payable", "Sources Of Funds(Liabilities)", "", 0, "Payable", "Liability"], ["Accounts Payable", "Sources Of Funds(Liabilities)", "", "", 0, "Payable", "Liability"],
["Accounts Receivable", "Application Of Funds(Assets)", "", 1, "Receivable", "Asset"], ["Accounts Receivable", "Application Of Funds(Assets)", "", "", 1, "Receivable", "Asset"],
["Stock Expenses", "Expenses", "", 0, "Stock Adjustment", "Expense"], ["Stock Expenses", "Expenses", "", "", 0, "Stock Adjustment", "Expense"],
["Sample Bank", "Bank Accounts", "", 0, "Bank", "Asset"], ["Sample Bank", "Bank Accounts", "", "", 0, "Bank", "Asset"],
["Cash", "Cash In Hand", "", 0, "Cash", "Asset"], ["Cash", "Cash In Hand", "", "", 0, "Cash", "Asset"],
["Stores", "Stock Assets", "", 0, "Stock", "Asset"], ["Stores", "Stock Assets", "", "", 0, "Stock", "Asset"],
] ]
for row in template: for row in template:

View File

@ -136,7 +136,7 @@ class PricingRule(Document):
for d in self.items: for d in self.items:
max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount") max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount")
if max_discount and flt(self.discount_percentage) > flt(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): def validate_price_list_with_currency(self):
if self.currency and self.for_price_list: if self.currency and self.for_price_list:

View File

@ -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 \ from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points 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.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.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
@ -76,6 +77,8 @@ class SalesInvoice(SellingController):
if not self.is_pos: if not self.is_pos:
self.so_dn_required() self.so_dn_required()
self.set_tax_withholding()
self.validate_proj_cust() self.validate_proj_cust()
self.validate_pos_return() self.validate_pos_return()
self.validate_with_previous_doc() self.validate_with_previous_doc()
@ -153,6 +156,32 @@ class SalesInvoice(SellingController):
if cost_center_company != self.company: 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))) 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): def before_save(self):
set_account_for_mode_of_payment(self) set_account_for_mode_of_payment(self)

View File

@ -10,6 +10,14 @@ frappe.ui.form.on('Subscription', {
} }
} }
}); });
frm.set_query('cost_center', function() {
return {
filters: {
company: frm.doc.company
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@ -7,9 +7,10 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"party_type", "party_type",
"status",
"cb_1",
"party", "party",
"cb_1",
"company",
"status",
"subscription_period", "subscription_period",
"start_date", "start_date",
"end_date", "end_date",
@ -44,80 +45,107 @@
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "cb_1", "fieldname": "cb_1",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Status", "label": "Status",
"no_copy": 1,
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "subscription_period", "fieldname": "subscription_period",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Subscription Period" "label": "Subscription Period",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "cancelation_date", "fieldname": "cancelation_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Cancelation Date", "label": "Cancelation Date",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fieldname": "trial_period_start", "fieldname": "trial_period_start",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Trial Period Start 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", "depends_on": "eval:doc.trial_period_start",
"fieldname": "trial_period_end", "fieldname": "trial_period_end",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Trial Period End Date", "label": "Trial Period End Date",
"set_only_once": 1 "set_only_once": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_11", "fieldname": "column_break_11",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "current_invoice_start", "fieldname": "current_invoice_start",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Current Invoice Start Date", "label": "Current Invoice Start Date",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "current_invoice_end", "fieldname": "current_invoice_end",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Current Invoice End Date", "label": "Current Invoice End Date",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"description": "Number of days that the subscriber has to pay invoices generated by this subscription", "description": "Number of days that the subscriber has to pay invoices generated by this subscription",
"fieldname": "days_until_due", "fieldname": "days_until_due",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Days Until Due" "label": "Days Until Due",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "cancel_at_period_end", "fieldname": "cancel_at_period_end",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Cancel At End Of Period" "label": "Cancel At End Of Period",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "generate_invoice_at_period_start", "fieldname": "generate_invoice_at_period_start",
"fieldtype": "Check", "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, "allow_on_submit": 1,
"fieldname": "sb_4", "fieldname": "sb_4",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Plans" "label": "Plans",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
@ -125,62 +153,84 @@
"fieldtype": "Table", "fieldtype": "Table",
"label": "Plans", "label": "Plans",
"options": "Subscription Plan Detail", "options": "Subscription Plan Detail",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)", "depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
"fieldname": "sb_1", "fieldname": "sb_1",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Taxes" "label": "Taxes",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "sb_2", "fieldname": "sb_2",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Discounts" "label": "Discounts",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "apply_additional_discount", "fieldname": "apply_additional_discount",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Apply Additional Discount On", "label": "Apply Additional Discount On",
"options": "\nGrand Total\nNet Total" "options": "\nGrand Total\nNet Total",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "cb_2", "fieldname": "cb_2",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "additional_discount_percentage", "fieldname": "additional_discount_percentage",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Additional DIscount Percentage" "label": "Additional DIscount Percentage",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "additional_discount_amount", "fieldname": "additional_discount_amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Additional DIscount Amount" "label": "Additional DIscount Amount",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.invoices", "depends_on": "eval:doc.invoices",
"fieldname": "sb_3", "fieldname": "sb_3",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Invoices" "label": "Invoices",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "invoices", "fieldname": "invoices",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Invoices", "label": "Invoices",
"options": "Subscription Invoice" "options": "Subscription Invoice",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"collapsible": 1, "collapsible": 1,
"fieldname": "accounting_dimensions_section", "fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Accounting Dimensions" "label": "Accounting Dimensions",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "dimension_col_break", "fieldname": "dimension_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "party_type", "fieldname": "party_type",
@ -188,7 +238,9 @@
"label": "Party Type", "label": "Party Type",
"options": "DocType", "options": "DocType",
"reqd": 1, "reqd": 1,
"set_only_once": 1 "set_only_once": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "party", "fieldname": "party",
@ -197,21 +249,27 @@
"label": "Party", "label": "Party",
"options": "party_type", "options": "party_type",
"reqd": 1, "reqd": 1,
"set_only_once": 1 "set_only_once": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.party_type === 'Customer'", "depends_on": "eval:doc.party_type === 'Customer'",
"fieldname": "sales_tax_template", "fieldname": "sales_tax_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Sales Taxes and Charges Template", "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'", "depends_on": "eval:doc.party_type === 'Supplier'",
"fieldname": "purchase_tax_template", "fieldname": "purchase_tax_template",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Purchase Taxes and Charges Template", "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", "default": "0",
@ -219,36 +277,55 @@
"fieldname": "follow_calendar_months", "fieldname": "follow_calendar_months",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Follow Calendar Months", "label": "Follow Calendar Months",
"set_only_once": 1 "set_only_once": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date", "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", "fieldname": "generate_new_invoices_past_due_date",
"fieldtype": "Check", "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", "fieldname": "end_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Subscription End Date", "label": "Subscription End Date",
"set_only_once": 1 "set_only_once": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "start_date", "fieldname": "start_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Subscription Start Date", "label": "Subscription Start Date",
"set_only_once": 1 "set_only_once": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "cost_center", "fieldname": "cost_center",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "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": [], "links": [],
"modified": "2020-06-25 10:52:52.265105", "modified": "2021-02-09 15:44:20.024789",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription", "name": "Subscription",

View File

@ -1,3 +1,4 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
@ -5,12 +6,13 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe import frappe
import erpnext
from frappe import _ from frappe import _
from frappe.model.document import Document 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 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.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
from erpnext import get_default_company
class Subscription(Document): class Subscription(Document):
def before_insert(self): def before_insert(self):
@ -243,6 +245,7 @@ class Subscription(Document):
self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval())
self.validate_end_date() self.validate_end_date()
self.validate_to_follow_calendar_months() self.validate_to_follow_calendar_months()
self.cost_center = erpnext.get_default_cost_center(self.get('company'))
def validate_trial_period(self): def validate_trial_period(self):
""" """
@ -304,6 +307,14 @@ class Subscription(Document):
doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice'
invoice = frappe.new_doc(doctype) 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.set_posting_time = 1
invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \ invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \
else self.current_invoice_end else self.current_invoice_end
@ -330,6 +341,7 @@ class Subscription(Document):
# for that reason # for that reason
items_list = self.get_items_from_plans(self.plans, prorate) items_list = self.get_items_from_plans(self.plans, prorate)
for item in items_list: for item in items_list:
item['cost_center'] = self.cost_center
invoice.append('items', item) invoice.append('items', item)
# Taxes # Taxes
@ -380,7 +392,8 @@ class Subscription(Document):
Returns the `Item`s linked to `Subscription Plan` Returns the `Item`s linked to `Subscription Plan`
""" """
if prorate: 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 = [] items = []
party = self.party party = self.party
@ -583,10 +596,13 @@ def get_calendar_months(billing_interval):
return calendar_months return calendar_months
def get_prorata_factor(period_end, period_start): def get_prorata_factor(period_end, period_start, is_prepaid):
diff = flt(date_diff(nowdate(), period_start) + 1) if is_prepaid:
plan_days = flt(date_diff(period_end, period_start) + 1) prorate_factor = 1
prorate_factor = diff / plan_days 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 return prorate_factor

View File

@ -321,7 +321,8 @@ class TestSubscription(unittest.TestCase):
self.assertEqual( self.assertEqual(
flt( 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), 2),
flt(prorate_factor, 2) flt(prorate_factor, 2)
) )
@ -561,9 +562,7 @@ class TestSubscription(unittest.TestCase):
current_inv = subscription.get_current_invoice() current_inv = subscription.get_current_invoice()
self.assertEqual(current_inv.status, "Unpaid") self.assertEqual(current_inv.status, "Unpaid")
diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) prorate_factor = 1
plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1)
prorate_factor = flt(diff / plan_days)
self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2)) self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2))

View File

@ -13,21 +13,28 @@
"fieldname": "document_type", "fieldname": "document_type",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Document Type ", "label": "Document Type ",
"no_copy": 1,
"options": "DocType", "options": "DocType",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "invoice", "fieldname": "invoice",
"fieldtype": "Dynamic Link", "fieldtype": "Dynamic Link",
"in_list_view": 1, "in_list_view": 1,
"label": "Invoice", "label": "Invoice",
"no_copy": 1,
"options": "document_type", "options": "document_type",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-06-01 22:23:54.462718", "modified": "2021-02-09 15:43:32.026233",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Subscription Invoice", "name": "Subscription Invoice",

View File

@ -12,37 +12,62 @@ from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document): class TaxWithholdingCategory(Document):
pass 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 = '' pan_no = ''
suppliers = [] parties = []
party_type, party = get_party_details(inv)
if not tax_withholding_category: 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: if not tax_withholding_category:
return return
# if tax_withholding_category passed as an argument but not pan_no
if 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 # Get others suppliers with the same PAN No
if 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: if not parties:
suppliers.append(ref_doc.supplier) 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: if not tax_details:
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') 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, if party_type == 'Customer' and not tax_details.cumulative_threshold:
tax_details, fy, ref_doc.posting_date, pan_no) # 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 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.")) frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
def get_tax_row(tax_details, tds_amount): def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
row = {
return {
"category": "Total", "category": "Total",
"add_deduct_tax": "Deduct",
"charge_type": "Actual", "charge_type": "Actual",
"account_head": tax_details.account_head, "tax_amount": tax_amount,
"description": tax_details.description, "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): if tax_deducted:
fiscal_year, year_start_date, year_end_date = fiscal_year_details # TCS already deducted on previous invoices
tds_amount = 0 # So, TCS will be calculated by 'Previous Row Total'
tds_deducted = 0
def _get_tds(amount, rate): taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head]
if amount <= 0: if taxes_excluding_tcs:
return 0 # chargeable amount is the total amount after other charges are applied
row.update({
return amount * rate / 100 "charge_type": "On Previous Row Total",
"row_id": len(taxes_excluding_tcs),
ldc_name = frappe.db.get_value('Lower Deduction Certificate', "rate": tax_details.rate
{ })
'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)
else: else:
tds_amount = _get_tds(net_total, tax_details.rate) # if only TCS is to be charged, then net total is chargeable amount
else: row.update({
supplier_credit_amount = frappe.get_all('Purchase Invoice', "charge_type": "On Net Total",
fields = ['sum(net_total)'], "rate": tax_details.rate
filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1) })
supplier_credit_amount = (supplier_credit_amount[0][0] return row
if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
jv_supplier_credit_amt = frappe.get_all('Journal Entry Account', def get_tax_row_for_tds(tax_details, tax_amount):
fields = ['sum(credit_in_account_currency)'], return {
filters = { "category": "Total",
'parent': ('in', vouchers), 'docstatus': 1, "charge_type": "Actual",
'party': ('in', suppliers), "tax_amount": tax_amount,
'reference_type': ('not in', ['Purchase Invoice']) "add_deduct_tax": "Deduct",
}, as_list=1) "description": tax_details.description,
"account_head": tax_details.account_head
}
supplier_credit_amount += (jv_supplier_credit_amt[0][0] def get_lower_deduction_certificate(fiscal_year, pan_no):
if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0) 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) vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
supplier_credit_amount -= debit_note_amount 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) tax_deducted = 0
or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)): 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, tax_amount = 0
ldc.certificate_limit): posting_date = inv.posting_date
tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate, if party_type == 'Supplier':
tax_details) 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: 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 return tds_amount
def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None): def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers):
condition = "fiscal_year=%s" % fiscal_year 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: if company:
condition += "and company =%s" % (company) filters['company'] = company
if from_date and to_date:
condition += "and posting_date between %s and %s" % (from_date, to_date)
## Appending the same supplier again if length of suppliers list is 1 return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0
## 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)))
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount): if current_amount < (certificate_limit - deducted_amount):

View File

@ -9,7 +9,7 @@ from frappe.utils import today
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Supplier Group"] test_dependencies = ["Supplier Group", "Customer Group"]
class TestTaxWithholdingCategory(unittest.TestCase): class TestTaxWithholdingCategory(unittest.TestCase):
@classmethod @classmethod
@ -18,6 +18,9 @@ class TestTaxWithholdingCategory(unittest.TestCase):
create_records() create_records()
create_tax_with_holding_category() create_tax_with_holding_category()
def tearDown(self):
cancel_invoices()
def test_cumulative_threshold_tds(self): def test_cumulative_threshold_tds(self):
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS") frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS")
invoices = [] invoices = []
@ -128,9 +131,59 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() 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): def create_purchase_invoice(**args):
# return sales invoice doc object # 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) args = frappe._dict(args)
pi = frappe.get_doc({ pi = frappe.get_doc({
@ -145,7 +198,7 @@ def create_purchase_invoice(**args):
"taxes": [], "taxes": [],
"items": [{ "items": [{
'doctype': 'Purchase Invoice Item', 'doctype': 'Purchase Invoice Item',
'item_code': item.name, 'item_code': item,
'qty': args.qty or 1, 'qty': args.qty or 1,
'rate': args.rate or 10000, 'rate': args.rate or 10000,
'cost_center': 'Main - _TC', 'cost_center': 'Main - _TC',
@ -156,6 +209,33 @@ def create_purchase_invoice(**args):
pi.save() pi.save()
return pi 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(): def create_records():
# create a new suppliers # create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']: for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
@ -168,7 +248,17 @@ def create_records():
"doctype": "Supplier", "doctype": "Supplier",
}).insert() }).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"): if not frappe.db.exists('Item', "TDS Item"):
frappe.get_doc({ frappe.get_doc({
"doctype": "Item", "doctype": "Item",
@ -178,7 +268,16 @@ def create_records():
"is_stock_item": 0, "is_stock_item": 0,
}).insert() }).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"): if not frappe.db.exists("Account", "TDS - _TC"):
frappe.get_doc({ frappe.get_doc({
'doctype': 'Account', 'doctype': 'Account',
@ -189,6 +288,17 @@ def create_records():
'root_type': 'Asset' 'root_type': 'Asset'
}).insert() }).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(): def create_tax_with_holding_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0] fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
@ -210,6 +320,23 @@ def create_tax_with_holding_category():
}] }]
}).insert() }).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 # Single thresold
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"): if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"):
frappe.get_doc({ frappe.get_doc({

View File

@ -196,7 +196,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
if not round_off_gle: if not round_off_gle:
for k in ["voucher_type", "voucher_no", "company", 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[k] = gl_map[0][k]
round_off_gle.update({ 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, "cost_center": round_off_cost_center,
"party_type": None, "party_type": None,
"party": None, "party": None,
"is_opening": "No",
"against_voucher_type": None, "against_voucher_type": None,
"against_voucher": None "against_voucher": None
}) })

View File

@ -897,17 +897,18 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa
frappe.db.sql("""delete from `tabGL Entry` frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no)) where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) 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) gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
for voucher_type, voucher_no in stock_vouchers: for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), []) existing_gle = gle.get((voucher_type, voucher_no), [])
voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no) voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account) expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle: if expected_gle:
if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle): if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True) voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else: else:
@ -953,16 +954,17 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
return gl_entries 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 matched = True
for entry in expected_gle: for entry in expected_gle:
account_existed = False account_existed = False
for e in existing_gle: for e in existing_gle:
if entry.account == e.account: if entry.account == e.account:
account_existed = True account_existed = True
if entry.account == e.account and entry.against_account == e.against_account \ if (entry.account == e.account and entry.against_account == e.against_account
and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \ and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
and (entry.debit != e.debit or entry.credit != e.credit): and ( flt(entry.debit, precision) != flt(e.debit, precision) or
flt(entry.credit, precision) != flt(e.credit, precision))):
matched = False matched = False
break break
if not account_existed: if not account_existed:

View File

@ -19,7 +19,6 @@
], ],
"fields": [ "fields": [
{ {
"depends_on": "eval:!doc.asset_category_name",
"fieldname": "asset_category_name", "fieldname": "asset_category_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
@ -67,7 +66,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-01-22 12:31:14.425319", "modified": "2021-02-24 15:05:38.621803",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Category", "name": "Asset Category",

View File

@ -117,7 +117,7 @@ def call_mws_method(mws_method, *args, **kwargs):
return response return response
except Exception as e: except Exception as e:
delay = math.pow(4, x) * 125 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) time.sleep(delay)
continue continue

View File

@ -393,6 +393,15 @@ payment_gateway_enabled = "erpnext.accounts.utils.create_payment_gateway_account
communication_doctypes = ["Customer", "Supplier"] 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 = { regional_overrides = {
'France': { 'France': {
'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method' 'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method'

View File

@ -138,7 +138,7 @@
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2020-08-27 14:30:28.995324", "modified": "2021-02-25 12:31:14.947865",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR Settings", "name": "HR Settings",
@ -155,5 +155,6 @@
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"track_changes": 1
} }

View File

@ -3,7 +3,7 @@
"allow_events_in_timeline": 0, "allow_events_in_timeline": 0,
"allow_guest_to_view": 0, "allow_guest_to_view": 0,
"allow_import": 0, "allow_import": 0,
"allow_rename": 0, "allow_rename": 1,
"autoname": "field:skill_name", "autoname": "field:skill_name",
"beta": 0, "beta": 0,
"creation": "2019-04-16 09:54:39.486915", "creation": "2019-04-16 09:54:39.486915",
@ -56,7 +56,7 @@
"issingle": 0, "issingle": 0,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2019-04-16 09:55:00.536328", "modified": "2021-02-24 09:55:00.536328",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Skill", "name": "Skill",

View File

@ -1,12 +1,13 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import getdate, get_time
from erpnext.stock.stock_ledger import update_entries_after from erpnext.stock.stock_ledger import update_entries_after
from erpnext.accounts.utils import update_gl_entries_after from erpnext.accounts.utils import update_gl_entries_after
def execute(): def execute():
frappe.reload_doc('stock', 'doctype', 'repost_item_valuation') 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(''' data = frappe.db.sql('''
SELECT SELECT
@ -40,7 +41,14 @@ def execute():
print("Reposting General Ledger Entries...") 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}): 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 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]

View File

@ -75,24 +75,27 @@ frappe.ui.form.on("Project", {
frm.add_custom_button(__('Cancelled'), () => { frm.add_custom_button(__('Cancelled'), () => {
frm.events.set_status(frm, 'Cancelled'); frm.events.set_status(frm, 'Cancelled');
}, __('Set Status')); }, __('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"), () => { if (frappe.model.can_read("Task")) {
frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', { frm.add_custom_button(__("Gantt Chart"), function () {
project: frm.doc.project_name frappe.route_options = {
}).then(() => { "project": frm.doc.name
frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_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) { create_duplicate: function(frm) {

View File

@ -37,7 +37,7 @@ class TestProject(unittest.TestCase):
task1 = task_exists("Test Template Task Parent") task1 = task_exists("Test Template Task Parent")
if not task1: 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") task2 = task_exists("Test Template Task Child 1")
if not task2: 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') 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(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(tasks[1].subject, 'Test Template Task Child 1')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3)) self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))

View File

@ -20,6 +20,7 @@
}, },
{ {
"columns": 6, "columns": 6,
"fetch_from": "task.subject",
"fieldname": "subject", "fieldname": "subject",
"fieldtype": "Read Only", "fieldtype": "Read Only",
"in_list_view": 1, "in_list_view": 1,
@ -28,7 +29,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-01-07 15:13:40.995071", "modified": "2021-02-24 15:18:49.095071",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Project Template Task", "name": "Project Template Task",

View File

@ -30,6 +30,7 @@ class Task(NestedSet):
def validate(self): def validate(self):
self.validate_dates() self.validate_dates()
self.validate_parent_expected_end_date()
self.validate_parent_project_dates() self.validate_parent_project_dates()
self.validate_progress() self.validate_progress()
self.validate_status() 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.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
frappe.bold("Actual End 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): def validate_parent_project_dates(self):
if not self.project or frappe.flags.in_test: if not self.project or frappe.flags.in_test:
return return

View File

@ -513,6 +513,7 @@ erpnext.utils.update_child_items = function(opts) {
}, { }, {
fieldtype:'Currency', fieldtype:'Currency',
fieldname:"rate", fieldname:"rate",
options: "currency",
default: 0, default: 0,
read_only: 0, read_only: 0,
in_list_view: 1, in_list_view: 1,

View File

@ -10,6 +10,7 @@ import sys
import json import json
import base64 import base64
import frappe import frappe
import six
import traceback import traceback
import io import io
from frappe import _, bold from frappe import _, bold
@ -108,11 +109,13 @@ def get_party_details(address_name):
pincode = 999999 pincode = 999999
return frappe._dict(dict( return frappe._dict(dict(
gstin=d.gstin, legal_name=d.address_title, gstin=d.gstin,
location=d.city, pincode=d.pincode, legal_name=sanitize_for_json(d.address_title),
location=sanitize_for_json(d.city),
pincode=d.pincode,
state_code=d.gst_state_number, state_code=d.gst_state_number,
address_line1=d.address_line1, address_line1=sanitize_for_json(d.address_line1),
address_line2=d.address_line2 address_line2=sanitize_for_json(d.address_line2)
)) ))
def get_gstin_details(gstin): def get_gstin_details(gstin):
@ -146,8 +149,11 @@ def get_overseas_address_details(address_name):
) )
return frappe._dict(dict( return frappe._dict(dict(
gstin='URP', legal_name=address_title, location=city, gstin='URP',
address_line1=address_line1, address_line2=address_line2, 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 pincode=999999, state_code=96, place_of_supply=96
)) ))
@ -160,7 +166,7 @@ def get_item_list(invoice):
item.update(d.as_dict()) item.update(d.as_dict())
item.sr_no = d.idx 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.qty = abs(item.qty)
item.discount_amount = 0 item.discount_amount = 0
@ -326,7 +332,7 @@ def make_einvoice(invoice):
buyer_details = get_overseas_address_details(invoice.customer_address) buyer_details = get_overseas_address_details(invoice.customer_address)
else: else:
buyer_details = get_party_details(invoice.customer_address) 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] place_of_supply = place_of_supply[:2]
buyer_details.update(dict(place_of_supply=place_of_supply)) 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, period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_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')) validations = json.loads(read_json('einv_validation'))
errors = validate_einvoice(validations, einvoice) errors = validate_einvoice(validations, einvoice)
@ -371,6 +377,18 @@ def make_einvoice(invoice):
return einvoice 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: <br> {}").format(snippet))
def validate_einvoice(validations, einvoice, errors=[]): def validate_einvoice(validations, einvoice, errors=[]):
for fieldname, field_validation in validations.items(): for fieldname, field_validation in validations.items():
value = einvoice.get(fieldname, None) value = einvoice.get(fieldname, None)
@ -798,6 +816,13 @@ class GSPConnector():
self.invoice.flags.ignore_validate = True self.invoice.flags.ignore_validate = True
self.invoice.save() 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() @frappe.whitelist()
def get_einvoice(doctype, docname): def get_einvoice(doctype, docname):
invoice = frappe.get_doc(doctype, docname) invoice = frappe.get_doc(doctype, docname)

View File

@ -16,6 +16,8 @@
"customer_name", "customer_name",
"gender", "gender",
"customer_type", "customer_type",
"pan",
"tax_withholding_category",
"default_bank_account", "default_bank_account",
"lead_name", "lead_name",
"image", "image",
@ -479,13 +481,25 @@
"fieldname": "dn_required", "fieldname": "dn_required",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Sales Invoice Creation Without Delivery Note" "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", "icon": "fa fa-user",
"idx": 363, "idx": 363,
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-01-06 19:35:25.418017", "modified": "2021-01-28 12:54:57.258959",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Customer", "name": "Customer",

View File

@ -498,10 +498,11 @@ erpnext.PointOfSale.Controller = class {
async on_cart_update(args) { async on_cart_update(args) {
frappe.dom.freeze(); frappe.dom.freeze();
let item_row = undefined;
try { try {
let { field, value, item } = args; let { field, value, item } = args;
const { item_code, batch_no, serial_no, uom } = item; 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" 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.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
this.update_cart_html(item_row); this.update_cart_html(item_row);
} }
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} finally { } finally {
frappe.dom.unfreeze(); frappe.dom.unfreeze();
return item_row;
} }
} }

View File

@ -472,7 +472,8 @@ erpnext.PointOfSale.ItemCart = class {
if (!frm) frm = this.events.get_frm(); if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.net_total); 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 => { const taxes = frm.doc.taxes.map(t => {
return { return {

View File

@ -152,6 +152,10 @@ erpnext.PointOfSale.ItemSelector = class {
this.item_group_field.toggle_label(false); this.item_group_field.toggle_label(false);
} }
set_search_value(value) {
$(this.search_field.$input[0]).val(value).trigger("input");
}
bind_events() { bind_events() {
const me = this; const me = this;
window.onScan = onScan; window.onScan = onScan;
@ -159,7 +163,7 @@ erpnext.PointOfSale.ItemSelector = class {
onScan: (sScancode) => { onScan: (sScancode) => {
if (this.search_field && this.$component.is(':visible')) { if (this.search_field && this.$component.is(':visible')) {
this.search_field.set_focus(); this.search_field.set_focus();
$(this.search_field.$input[0]).val(sScancode).trigger("input"); this.set_search_value(sScancode);
this.barcode_scanned = true; this.barcode_scanned = true;
} }
} }
@ -178,6 +182,7 @@ erpnext.PointOfSale.ItemSelector = class {
uom = uom === "undefined" ? undefined : uom; uom = uom === "undefined" ? undefined : uom;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, 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) => { this.search_field.$input.on('input', (e) => {

View File

@ -223,7 +223,8 @@ erpnext.PointOfSale.Payment = class {
if (success) { if (success) {
title = __("Payment Received"); 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(); frappe.dom.unfreeze();
message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]); message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
this.events.submit_invoice(); this.events.submit_invoice();
@ -243,7 +244,8 @@ erpnext.PointOfSale.Payment = class {
auto_set_remaining_amount() { auto_set_remaining_amount() {
const doc = this.events.get_frm().doc; 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; const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
if (!current_value && remaining_amount > 0 && this.selected_mode) { if (!current_value && remaining_amount > 0 && this.selected_mode) {
this.selected_mode.set_value(remaining_amount); this.selected_mode.set_value(remaining_amount);
@ -389,7 +391,7 @@ erpnext.PointOfSale.Payment = class {
} }
attach_cash_shortcuts(doc) { 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 currency = doc.currency;
const shortcuts = this.get_cash_shortcuts(flt(grand_total)); const shortcuts = this.get_cash_shortcuts(flt(grand_total));
@ -499,7 +501,8 @@ erpnext.PointOfSale.Payment = class {
update_totals_section(doc) { update_totals_section(doc) {
if (!doc) doc = this.events.get_frm().doc; if (!doc) doc = this.events.get_frm().doc;
const paid_amount = doc.paid_amount; 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 change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
const currency = doc.currency; const currency = doc.currency;
const label = change ? __('Change') : __('To Be Paid'); const label = change ? __('Change') : __('To Be Paid');
@ -507,7 +510,7 @@ erpnext.PointOfSale.Payment = class {
this.$totals.html( this.$totals.html(
`<div class="col"> `<div class="col">
<div class="total-label">Grand Total</div> <div class="total-label">Grand Total</div>
<div class="value">${format_currency(doc.grand_total, currency)}</div> <div class="value">${format_currency(grand_total, currency)}</div>
</div> </div>
<div class="seperator-y"></div> <div class="seperator-y"></div>
<div class="col"> <div class="col">

View File

@ -64,6 +64,10 @@ def get_warehouse_account(warehouse, warehouse_account=None):
if not account and warehouse.company: if not account and warehouse.company:
account = get_company_default_inventory_account(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: 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}") frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}")
.format(warehouse.name, warehouse.company)) .format(warehouse.name, warehouse.company))

View File

@ -521,8 +521,7 @@
"fieldname": "has_variants", "fieldname": "has_variants",
"fieldtype": "Check", "fieldtype": "Check",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Has Variants", "label": "Has Variants"
"no_copy": 1
}, },
{ {
"default": "Item Attribute", "default": "Item Attribute",
@ -538,7 +537,6 @@
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 1, "hidden": 1,
"label": "Attributes", "label": "Attributes",
"no_copy": 1,
"options": "Item Variant Attribute" "options": "Item Variant Attribute"
}, },
{ {
@ -1068,7 +1066,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"max_attachments": 1, "max_attachments": 1,
"modified": "2021-01-25 20:49:50.222976", "modified": "2021-02-18 14:00:19.668049",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -295,7 +295,8 @@ class PurchaseReceipt(BuyingController):
"against": warehouse_account[d.warehouse]["account"], "against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center, "cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), "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"]), "credit_in_account_currency": flt(amount["amount"]),
"project": d.project "project": d.project
}, item=d)) }, item=d))

View File

@ -276,9 +276,10 @@ class StockEntry(StockController):
item_wise_qty.setdefault(d.item_code, []).append(d.qty) item_wise_qty.setdefault(d.item_code, []).append(d.qty)
for item_code, qty_list in iteritems(item_wise_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") 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): def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)): if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):

View File

@ -49,8 +49,8 @@ frappe.ui.form.on("Issue", {
}, },
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") { if (frm.doc.status !== "Closed") {
if (frm.doc.service_level_agreement) { if (frm.doc.service_level_agreement && frm.doc.agreement_status === "Ongoing") {
frappe.call({ frappe.call({
"method": "frappe.client.get", "method": "frappe.client.get",
args: { args: {