Merge branch 'version-12-hotfix' into v12-pre-release

This commit is contained in:
Nabin Hait
2021-03-16 11:03:25 +05:30
28 changed files with 208 additions and 108 deletions

View File

@@ -21,6 +21,14 @@ frappe.ui.form.on("Bank Reconciliation", {
}; };
}); });
frm.set_query("bank_account", function() {
return {
"filters": {
"is_company_account": 1
}
};
});
frm.set_value("from_date", frappe.datetime.month_start()); frm.set_value("from_date", frappe.datetime.month_start());
frm.set_value("to_date", frappe.datetime.month_end()); frm.set_value("to_date", frappe.datetime.month_end());
}, },

View File

@@ -207,11 +207,11 @@ class JournalEntry(AccountsController):
if d.reference_type=="Journal Entry": if d.reference_type=="Journal Entry":
account_root_type = frappe.db.get_value("Account", d.account, "root_type") account_root_type = frappe.db.get_value("Account", d.account, "root_type")
if account_root_type == "Asset" and flt(d.debit) > 0: if account_root_type == "Asset" and flt(d.debit) > 0:
frappe.throw(_("For {0}, only credit accounts can be linked against another debit entry") frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets credited")
.format(d.account)) .format(d.idx, d.account))
elif account_root_type == "Liability" and flt(d.credit) > 0: elif account_root_type == "Liability" and flt(d.credit) > 0:
frappe.throw(_("For {0}, only debit accounts can be linked against another credit entry") frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets debited")
.format(d.account)) .format(d.idx, d.account))
if d.reference_name == self.name: if d.reference_name == self.name:
frappe.throw(_("You can not enter current voucher in 'Against Journal Entry' column")) frappe.throw(_("You can not enter current voucher in 'Against Journal Entry' column"))

View File

@@ -531,7 +531,8 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Title", "label": "Title",
"print_hide": 1 "print_hide": 1,
"read_only": 1
}, },
{ {
"depends_on": "party", "depends_on": "party",
@@ -575,7 +576,7 @@
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"modified": "2019-11-06 12:59:43.151721", "modified": "2021-03-10 13:05:16.958866",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Entry", "name": "Payment Entry",
@@ -619,4 +620,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "title", "title_field": "title",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -441,6 +441,10 @@ class PaymentEntry(AccountsController):
.format(total_negative_outstanding), InvalidPaymentEntry) .format(total_negative_outstanding), InvalidPaymentEntry)
def set_title(self): def set_title(self):
if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import.
return
if self.payment_type in ("Receive", "Pay"): if self.payment_type in ("Receive", "Pay"):
self.title = self.party self.title = self.party
else: else:

View File

@@ -131,7 +131,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

@@ -252,7 +252,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({
@@ -264,6 +264,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

@@ -224,8 +224,7 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, fiscal_year, filters): def calculate_values(accounts_by_name, gl_entries_by_account, companies, fiscal_year, filters):
for entries in gl_entries_by_account.values(): for entries in gl_entries_by_account.values():
for entry in entries: for entry in entries:
key = entry.account_number or entry.account_name d = accounts_by_name.get(entry.account_name)
d = accounts_by_name.get(key)
if d: if d:
for company in companies: for company in companies:
# check if posting date is within the period # check if posting date is within the period
@@ -240,7 +239,8 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts""" """accumulate children's values in parent accounts"""
for d in reversed(accounts): for d in reversed(accounts):
if d.parent_account: if d.parent_account:
account = d.parent_account.split('-')[0].strip() account = d.parent_account_name
if not accounts_by_name.get(account): if not accounts_by_name.get(account):
continue continue
@@ -251,16 +251,34 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
accounts_by_name[account]["opening_balance"] = \ accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters): def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters) accounts = get_accounts(root_type, filters)
if not accounts: if not accounts:
return None, None return None, None
accounts = update_parent_account_names(accounts)
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name return accounts, accounts_by_name
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.
parent_name is `name` of parent account which could have other prefix
of account_number and suffix of company abbr. This function adds key called
`parent_account_name` which does not have such prefix/suffix.
"""
name_to_account_map = { d.name : d.account_name for d in accounts }
for account in accounts:
if account.parent_account:
account["parent_account_name"] = name_to_account_map[account.parent_account]
return accounts
def get_companies(filters): def get_companies(filters):
companies = {} companies = {}
all_companies = get_subsidiary_companies(filters.get('company')) all_companies = get_subsidiary_companies(filters.get('company'))
@@ -367,9 +385,9 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
convert_to_presentation_currency(gl_entries, currency_info) convert_to_presentation_currency(gl_entries, currency_info)
for entry in gl_entries: for entry in gl_entries:
key = entry.account_number or entry.account_name account_name = entry.account_name
validate_entries(key, entry, accounts_by_name, accounts) validate_entries(account_name, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry) gl_entries_by_account.setdefault(account_name, []).append(entry)
return gl_entries_by_account return gl_entries_by_account
@@ -438,8 +456,7 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {} parent_children_map = {}
accounts_by_name = {} accounts_by_name = {}
for d in accounts: for d in accounts:
key = d.account_number or d.account_name accounts_by_name[d.account_name] = d
accounts_by_name[key] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d) parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = [] filtered_accounts = []

View File

@@ -3,6 +3,7 @@
"description": "Settings for Buying Module", "description": "Settings for Buying Module",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"engine": "InnoDB",
"field_order": [ "field_order": [
"supp_master_name", "supp_master_name",
"supplier_group", "supplier_group",
@@ -92,7 +93,7 @@
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"modified": "2019-08-20 13:13:09.055189", "modified": "2021-03-02 18:16:03.947813",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",
@@ -107,5 +108,8 @@
"share": 1, "share": 1,
"write": 1 "write": 1
} }
] ],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
} }

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="Method {} failed".format(mws_method.__name__))
time.sleep(delay) time.sleep(delay)
continue continue

View File

@@ -1,5 +1,4 @@
{ {
"actions": [],
"creation": "2018-10-25 10:02:48.656165", "creation": "2018-10-25 10:02:48.656165",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
@@ -69,8 +68,7 @@
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "modified": "2021-03-02 18:05:50.794105",
"modified": "2020-10-29 20:24:56.916104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Plaid Settings", "name": "Plaid Settings",
@@ -88,5 +86,6 @@
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"track_changes": 1
} }

View File

@@ -2,6 +2,7 @@
"creation": "2015-05-18 05:21:07.270859", "creation": "2015-05-18 05:21:07.270859",
"doctype": "DocType", "doctype": "DocType",
"document_type": "System", "document_type": "System",
"engine": "InnoDB",
"field_order": [ "field_order": [
"status_html", "status_html",
"enable_shopify", "enable_shopify",
@@ -258,8 +259,8 @@
} }
], ],
"issingle": 1, "issingle": 1,
"modified": "2020-05-28 12:32:11.384757", "modified": "2021-03-02 18:06:00.868688",
"modified_by": "umair@erpnext.com", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "Shopify Settings", "name": "Shopify Settings",
"owner": "Administrator", "owner": "Administrator",
@@ -276,5 +277,6 @@
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
} "track_changes": 1
}

View File

@@ -261,6 +261,9 @@ doc_events = {
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply'] 'validate': ['erpnext.regional.india.utils.set_place_of_supply']
}, },
('Sales Invoice', 'Purchase Invoice'): {
'validate': ['erpnext.regional.india.utils.validate_document_name']
},
"Contact": { "Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue", "on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information", "after_insert": "erpnext.communication.doctype.call_log.call_log.set_caller_information",

View File

@@ -175,7 +175,7 @@
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2019-12-31 14:28:32.004121", "modified": "2021-02-25 13:06:37.978785",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR Settings", "name": "HR Settings",
@@ -192,5 +192,6 @@
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"track_changes": 1
} }

View File

@@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals import erpnext
import frappe, erpnext import frappe
from frappe import _
from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr, add_days, today
from frappe.model.document import Document
from frappe.desk.form import assign_to
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from frappe import _
from frappe.desk.form import assign_to
from frappe.model.document import Document
from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
get_datetime, getdate, nowdate, today, unique)
class DuplicateDeclarationError(frappe.ValidationError): pass class DuplicateDeclarationError(frappe.ValidationError): pass
class EmployeeBoardingController(Document): class EmployeeBoardingController(Document):
''' '''
Create the project and the task for the boarding process Create the project and the task for the boarding process
@@ -48,27 +51,38 @@ class EmployeeBoardingController(Document):
continue continue
task = frappe.get_doc({ task = frappe.get_doc({
"doctype": "Task", "doctype": "Task",
"project": self.project, "project": self.project,
"subject": activity.activity_name + " : " + self.employee_name, "subject": activity.activity_name + " : " + self.employee_name,
"description": activity.description, "description": activity.description,
"department": self.department, "department": self.department,
"company": self.company, "company": self.company,
"task_weight": activity.task_weight "task_weight": activity.task_weight
}).insert(ignore_permissions=True) }).insert(ignore_permissions=True)
activity.db_set("task", task.name) activity.db_set("task", task.name)
users = [activity.user] if activity.user else [] users = [activity.user] if activity.user else []
if activity.role: if activity.role:
user_list = frappe.db.sql_list('''select distinct(parent) from `tabHas Role` user_list = frappe.db.sql_list('''
where parenttype='User' and role=%s''', activity.role) SELECT
users = users + user_list DISTINCT(has_role.parent)
FROM
`tabHas Role` has_role
LEFT JOIN `tabUser` user
ON has_role.parent = user.name
WHERE
has_role.parenttype = 'User'
AND user.enabled = 1
AND has_role.role = %s
''', activity.role)
users = unique(users + user_list)
if "Administrator" in users: if "Administrator" in users:
users.remove("Administrator") users.remove("Administrator")
# assign the task the users # assign the task the users
if users: if users:
self.assign_task_to_users(task, set(users)) self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users): def assign_task_to_users(self, task, users):
for user in users: for user in users:
@@ -453,4 +467,4 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co
}, as_dict=True) }, as_dict=True)
if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0: if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0:
total_claimed_amount = sum_of_claimed_amount[0].total_amount total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount return total_claimed_amount

View File

@@ -88,7 +88,7 @@ def get_bom_stock(filters):
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1) GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records(): def get_manufacturer_records():
details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no, parent"]) details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
manufacture_details = frappe._dict() manufacture_details = frappe._dict()
for detail in details: for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {}) dic = manufacture_details.setdefault(detail.get('parent'), {})

View File

@@ -75,23 +75,23 @@ 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")) { if (frappe.model.can_read("Task")) {
frm.add_custom_button(__("Gantt Chart"), function () { frm.add_custom_button(__("Gantt Chart"), function () {
frappe.route_options = { frappe.route_options = {
"project": frm.doc.name "project": frm.doc.name
}; };
frappe.set_route("List", "Task", "Gantt"); 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);
}); });
});
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);
});
});
}
} }
}, },

View File

@@ -507,6 +507,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

@@ -23,7 +23,7 @@ def validate_einvoice_fields(doc):
invalid_doctype = doc.doctype != 'Sales Invoice' invalid_doctype = doc.doctype != 'Sales Invoice'
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
no_taxes_applied = len(doc.get('taxes', [])) == 0 no_taxes_applied = not doc.get('taxes')
if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied: if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
return return
@@ -160,7 +160,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 = d.item_name.replace('"', '\\"') item.description = json.dumps(d.item_name)[1:-1]
item.qty = abs(item.qty) item.qty = abs(item.qty)
item.discount_amount = 0 item.discount_amount = 0
@@ -196,9 +196,11 @@ def update_item_taxes(invoice, item):
item[attr] = 0 item[attr] = 0
for t in invoice.taxes: for t in invoice.taxes:
# this contains item wise tax rate & tax amount (incl. discount) is_applicable = t.tax_amount and t.account_head in gst_accounts_list
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) if is_applicable:
if t.account_head in gst_accounts_list: # this contains item wise tax rate & tax amount (incl. discount)
item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
item_tax_rate = item_tax_detail[0] item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount # item tax amount excluding discount amount
item_tax_amount = (item_tax_rate / 100) * item.base_net_amount item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
@@ -223,7 +225,7 @@ def get_invoice_value_details(invoice):
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total) invoice_value_details.base_total = abs(invoice.base_total)
invoice_value_details.invoice_discount_amt = invoice.base_discount_amount invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount)
else: else:
invoice_value_details.base_total = abs(invoice.base_net_total) invoice_value_details.base_total = abs(invoice.base_net_total)
# since tax already considers discount amount # since tax already considers discount amount

View File

@@ -2,7 +2,7 @@ from __future__ import unicode_literals
import frappe, re, json import frappe, re, json
from frappe import _ from frappe import _
import erpnext import erpnext
from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
from erpnext.regional.india import states, state_numbers from erpnext.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount, calculate_outstanding_amount from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount, calculate_outstanding_amount
from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.controllers.accounts_controller import get_taxes_and_charges
@@ -15,6 +15,13 @@ from erpnext.accounts.utils import get_account_currency
from frappe.contacts.doctype.address.address import get_address_display from frappe.contacts.doctype.address.address import get_address_display
from frappe.model.utils import get_fetch_values from frappe.model.utils import get_fetch_values
GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") #alphanumeric and - /
GSTIN_FORMAT = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
GSTIN_UIN_FORMAT = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
def validate_gstin_for_india(doc, method): def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state: if hasattr(doc, 'gst_state') and doc.gst_state:
doc.gst_state_number = state_numbers[doc.gst_state] doc.gst_state_number = state_numbers[doc.gst_state]
@@ -38,12 +45,10 @@ def validate_gstin_for_india(doc, method):
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters.")) frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
if gst_category and gst_category == 'UIN Holders': if gst_category and gst_category == 'UIN Holders':
p = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}") if not GSTIN_UIN_FORMAT.match(doc.gstin):
if not p.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers")) frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
else: else:
p = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$") if not GSTIN_FORMAT.match(doc.gstin):
if not p.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN.")) frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
validate_gstin_check_digit(doc.gstin) validate_gstin_check_digit(doc.gstin)
@@ -162,6 +167,20 @@ def set_transporter_address(doc, method=None):
doc.transporter_address = transporter_address doc.transporter_address = transporter_address
doc.transporter_address_display = get_address_display(transporter_address) doc.transporter_address_display = get_address_display(transporter_address)
def validate_document_name(doc, method=None):
"""Validate GST invoice number requirements."""
country = frappe.get_cached_value("Company", doc.company, "country")
# Date was chosen as start of next FY to avoid irritating current users.
if country != "India" or getdate(doc.posting_date) < getdate("2021-04-01"):
return
if len(doc.name) > 16:
frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series."))
if not GST_INVOICE_NUMBER_FORMAT.match(doc.name):
frappe.throw(_("Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series."))
# don't remove this function it is used in tests # don't remove this function it is used in tests
def test_method(): def test_method():
'''test function''' '''test function'''
@@ -715,25 +734,12 @@ def update_grand_total_for_rcm(doc, method):
if country != 'India': if country != 'India':
return return
if not doc.total_taxes_and_charges: gst_tax, base_gst_tax = get_gst_tax_amount(doc)
if not base_gst_tax:
return return
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account')
base_gst_tax = 0
gst_tax = 0
for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
base_gst_tax += tax.base_tax_amount_after_discount_amount
gst_tax += tax.tax_amount_after_discount_amount
doc.taxes_and_charges_added -= gst_tax doc.taxes_and_charges_added -= gst_tax
doc.total_taxes_and_charges -= gst_tax doc.total_taxes_and_charges -= gst_tax
doc.base_taxes_and_charges_added -= base_gst_tax doc.base_taxes_and_charges_added -= base_gst_tax
@@ -765,6 +771,11 @@ def make_regional_gl_entries(gl_entries, doc):
if country != 'India': if country != 'India':
return gl_entries return gl_entries
gst_tax, base_gst_tax = get_gst_tax_amount(doc)
if not base_gst_tax:
return gl_entries
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company) gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \ gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
@@ -792,3 +803,21 @@ def make_regional_gl_entries(gl_entries, doc):
) )
return gl_entries return gl_entries
def get_gst_tax_amount(doc):
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account', []) + gst_accounts.get('sgst_account', []) \
+ gst_accounts.get('igst_account', [])
base_gst_tax = 0
gst_tax = 0
for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
base_gst_tax += tax.base_tax_amount_after_discount_amount
gst_tax += tax.tax_amount_after_discount_amount
return gst_tax, base_gst_tax

View File

@@ -236,6 +236,7 @@ class Gstr1Report(object):
self.cgst_sgst_invoices = [] self.cgst_sgst_invoices = []
unidentified_gst_accounts = [] unidentified_gst_accounts = []
unidentified_gst_accounts_invoice = []
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details: for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
if account in self.gst_accounts.cess_account: if account in self.gst_accounts.cess_account:
self.invoice_cess.setdefault(parent, tax_amount) self.invoice_cess.setdefault(parent, tax_amount)
@@ -251,6 +252,7 @@ class Gstr1Report(object):
if not (cgst_or_sgst or account in self.gst_accounts.igst_account): if not (cgst_or_sgst or account in self.gst_accounts.igst_account):
if "gst" in account.lower() and account not in unidentified_gst_accounts: if "gst" in account.lower() and account not in unidentified_gst_accounts:
unidentified_gst_accounts.append(account) unidentified_gst_accounts.append(account)
unidentified_gst_accounts_invoice.append(parent)
continue continue
for item_code, tax_amounts in item_wise_tax_detail.items(): for item_code, tax_amounts in item_wise_tax_detail.items():
@@ -273,7 +275,7 @@ class Gstr1Report(object):
# Build itemised tax for export invoices where tax table is blank # Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items): for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate \ if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax": and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys()) self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())

View File

@@ -573,7 +573,7 @@
"issingle": 1, "issingle": 1,
"istable": 0, "istable": 0,
"max_attachments": 0, "max_attachments": 0,
"modified": "2018-06-25 12:56:16.332039", "modified": "2021-03-02 17:35:14.084342",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",
@@ -605,7 +605,7 @@
"show_name_in_global_search": 0, "show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "track_changes": 1,
"track_seen": 0, "track_seen": 0,
"track_views": 0 "track_views": 0
} }

View File

@@ -26,8 +26,9 @@ def delete_company_transactions(company_name):
tabDocField where fieldtype='Link' and options='Company'"""): tabDocField where fieldtype='Link' and options='Company'"""):
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget", if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
"Party Account", "Employee", "Sales Taxes and Charges Template", "Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", 'BOM', "Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Item default", "Customer", "Supplier"): "Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
"Item Default", "Customer", "Supplier", "GST Account"):
delete_for_doctype(doctype, company_name) delete_for_doctype(doctype, company_name)
# reset company values # reset company values

View File

@@ -17,7 +17,7 @@ frappe.ui.form.on('Global Defaults', {
method: "frappe.client.get_list", method: "frappe.client.get_list",
args: { args: {
doctype: "UOM Conversion Factor", doctype: "UOM Conversion Factor",
filters: { "category": "Length" }, filters: { "category": __("Length") },
fields: ["to_uom"], fields: ["to_uom"],
limit_page_length: 500 limit_page_length: 500
}, },

View File

@@ -161,7 +161,7 @@
"icon": "fa fa-shopping-cart", "icon": "fa fa-shopping-cart",
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"modified": "2020-07-07 02:13:23.175604", "modified": "2021-03-02 18:05:56.721122",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Shopping Cart", "module": "Shopping Cart",
"name": "Shopping Cart Settings", "name": "Shopping Cart Settings",
@@ -178,5 +178,6 @@
} }
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"track_changes": 1
} }

View File

@@ -156,7 +156,8 @@ erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callb
fieldtype: 'Float', description: __('Available {0}', [actual_qty]) }, fieldtype: 'Float', description: __('Available {0}', [actual_qty]) },
{fieldname: 'rate', label: __('Rate'), fieldtype: 'Currency', hidden: 1 }, {fieldname: 'rate', label: __('Rate'), fieldtype: 'Currency', hidden: 1 },
], ],
}) });
var submitted = false;
dialog.show(); dialog.show();
dialog.get_field('item_code').set_input(item); dialog.get_field('item_code').set_input(item);
@@ -180,6 +181,7 @@ erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callb
} }
dialog.set_primary_action(__('Submit'), function() { dialog.set_primary_action(__('Submit'), function() {
if(submitted) return;
var values = dialog.get_values(); var values = dialog.get_values();
if(!values) { if(!values) {
return; return;
@@ -192,6 +194,7 @@ erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callb
frappe.msgprint(__('Source and target warehouse must be different')); frappe.msgprint(__('Source and target warehouse must be different'));
} }
submitted = true;
frappe.call({ frappe.call({
method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry', method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry',
args: values, args: values,

View File

@@ -177,7 +177,7 @@ class Item(WebsiteGenerator):
if not self.valuation_rate and self.standard_rate: if not self.valuation_rate and self.standard_rate:
self.valuation_rate = self.standard_rate self.valuation_rate = self.standard_rate
if not self.valuation_rate: if not self.valuation_rate and not self.is_customer_provided_item:
frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered")) frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered"))
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry

View File

@@ -115,10 +115,17 @@ def get_serial_nos_data_after_transactions(args):
order by posting_date, posting_time asc """, args, as_dict=1) order by posting_date, posting_time asc """, args, as_dict=1)
for d in data: for d in data:
if d.actual_qty > 0: for sn in get_serial_nos_data(d.serial_no):
serial_nos.extend(get_serial_nos_data(d.serial_no)) if d.actual_qty > 0:
else: if sn not in serial_nos:
serial_nos = list(set(serial_nos) - set(get_serial_nos_data(d.serial_no))) serial_nos.append(sn)
else:
serial_nos.remove(sn)
elif d.actual_qty < 0:
if sn in serial_nos:
serial_nos.remove(sn)
else:
serial_nos.append(sn)
return '\n'.join(serial_nos) return '\n'.join(serial_nos)

View File

@@ -39,8 +39,8 @@ frappe.ui.form.on("Issue", {
refresh: function (frm) { refresh: function (frm) {
if (frm.doc.status !== "Closed" && frm.doc.agreement_fulfilled === "Ongoing") { if (frm.doc.status !== "Closed") {
if (frm.doc.service_level_agreement) { if (frm.doc.service_level_agreement && frm.doc.agreement_fulfilled === "Ongoing") {
set_time_to_resolve_and_response(frm); set_time_to_resolve_and_response(frm);
} }