mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 12:39:18 +00:00
Merge branch 'v12-pre-release' into version-12
This commit is contained in:
@@ -5,7 +5,7 @@ import frappe
|
|||||||
from erpnext.hooks import regional_overrides
|
from erpnext.hooks import regional_overrides
|
||||||
from frappe.utils import getdate
|
from frappe.utils import getdate
|
||||||
|
|
||||||
__version__ = '12.12.1'
|
__version__ = '12.13.0'
|
||||||
|
|
||||||
def get_default_company(user=None):
|
def get_default_company(user=None):
|
||||||
'''Get default company for user'''
|
'''Get default company for user'''
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
"default": "1",
|
"default": "1",
|
||||||
"fieldname": "unlink_advance_payment_on_cancelation_of_order",
|
"fieldname": "unlink_advance_payment_on_cancelation_of_order",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Unlink Advance Payment on Cancelation of Order"
|
"label": "Unlink Advance Payment on Cancellation of Order"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "1",
|
"default": "1",
|
||||||
@@ -179,7 +179,7 @@
|
|||||||
"icon": "icon-cog",
|
"icon": "icon-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"modified": "2020-03-11 13:09:26.235848",
|
"modified": "2020-10-08 09:40:12.121145",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
|||||||
@@ -1,347 +1,122 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 0,
|
|
||||||
"autoname": "field:year",
|
"autoname": "field:year",
|
||||||
"beta": 0,
|
|
||||||
"creation": "2013-01-22 16:50:25",
|
"creation": "2013-01-22 16:50:25",
|
||||||
"custom": 0,
|
|
||||||
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
|
"description": "**Fiscal Year** represents a Financial Year. All accounting entries and other major transactions are tracked against **Fiscal Year**.",
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
"editable_grid": 0,
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"year",
|
||||||
|
"disabled",
|
||||||
|
"is_short_year",
|
||||||
|
"year_start_date",
|
||||||
|
"year_end_date",
|
||||||
|
"companies",
|
||||||
|
"auto_created"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"description": "For e.g. 2012, 2012-13",
|
"description": "For e.g. 2012, 2012-13",
|
||||||
"fieldname": "year",
|
"fieldname": "year",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Year Name",
|
"label": "Year Name",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"oldfieldname": "year",
|
"oldfieldname": "year",
|
||||||
"oldfieldtype": "Data",
|
"oldfieldtype": "Data",
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 0,
|
"unique": 1
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "disabled",
|
"fieldname": "disabled",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"label": "Disabled"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Disabled",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "year_start_date",
|
"fieldname": "year_start_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Year Start Date",
|
"label": "Year Start Date",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"oldfieldname": "year_start_date",
|
"oldfieldname": "year_start_date",
|
||||||
"oldfieldtype": "Date",
|
"oldfieldtype": "Date",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "year_end_date",
|
"fieldname": "year_end_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Year End Date",
|
"label": "Year End Date",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "companies",
|
"fieldname": "companies",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Companies",
|
"label": "Companies",
|
||||||
"length": 0,
|
"options": "Fiscal Year Company"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Fiscal Year Company",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "auto_created",
|
"fieldname": "auto_created",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Auto Created",
|
"label": "Auto Created",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
},
|
||||||
"remember_last_selected_value": 0,
|
{
|
||||||
"report_hide": 0,
|
"default": "0",
|
||||||
"reqd": 0,
|
"description": "Less than 12 months.",
|
||||||
"search_index": 0,
|
"fieldname": "is_short_year",
|
||||||
"set_only_once": 0,
|
"fieldtype": "Check",
|
||||||
"translatable": 0,
|
"label": "Is Short Year",
|
||||||
"unique": 0
|
"set_only_once": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"icon": "fa fa-calendar",
|
"icon": "fa fa-calendar",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"image_view": 0,
|
"modified": "2020-10-03 18:22:04.161315",
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2018-04-25 14:21:41.273354",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Fiscal Year",
|
"name": "Fiscal Year",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 1,
|
"email": 1,
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 1,
|
"print": 1,
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 0,
|
|
||||||
"delete": 0,
|
|
||||||
"email": 0,
|
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
"role": "Sales User"
|
||||||
"role": "Sales User",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 0,
|
|
||||||
"delete": 0,
|
|
||||||
"email": 0,
|
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
"role": "Purchase User"
|
||||||
"role": "Purchase User",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 0,
|
|
||||||
"delete": 0,
|
|
||||||
"email": 0,
|
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
"role": "Accounts User"
|
||||||
"role": "Accounts User",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 0,
|
|
||||||
"delete": 0,
|
|
||||||
"email": 0,
|
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
"role": "Stock User"
|
||||||
"role": "Stock User",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 0,
|
|
||||||
"delete": 0,
|
|
||||||
"email": 0,
|
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 0,
|
"role": "Employee"
|
||||||
"role": "Employee",
|
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 0,
|
|
||||||
"submit": 0,
|
|
||||||
"write": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "name",
|
"sort_field": "name",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC"
|
||||||
"track_changes": 0,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@@ -36,6 +36,11 @@ class FiscalYear(Document):
|
|||||||
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
|
frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
|
||||||
|
|
||||||
def validate_dates(self):
|
def validate_dates(self):
|
||||||
|
if self.is_short_year:
|
||||||
|
# Fiscal Year can be shorter than one year, in some jurisdictions
|
||||||
|
# under certain circumstances. For example, in the USA and Germany.
|
||||||
|
return
|
||||||
|
|
||||||
if getdate(self.year_start_date) > getdate(self.year_end_date):
|
if getdate(self.year_start_date) > getdate(self.year_end_date):
|
||||||
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
|
frappe.throw(_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
|
||||||
FiscalYearIncorrectDate)
|
FiscalYearIncorrectDate)
|
||||||
|
|||||||
@@ -58,5 +58,12 @@
|
|||||||
"year": "_Test Fiscal Year 2021",
|
"year": "_Test Fiscal Year 2021",
|
||||||
"year_end_date": "2021-12-31",
|
"year_end_date": "2021-12-31",
|
||||||
"year_start_date": "2021-01-01"
|
"year_start_date": "2021-01-01"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"doctype": "Fiscal Year",
|
||||||
|
"year": "_Test Short Fiscal Year 2021",
|
||||||
|
"is_short_year": 1,
|
||||||
|
"year_end_date": "2021-12-31",
|
||||||
|
"year_start_date": "2021-04-01"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -31,13 +31,19 @@ apply_on_table = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
def get_pricing_rules(args, doc=None):
|
def get_pricing_rules(args, doc=None):
|
||||||
pricing_rules = []
|
pricing_rules_all = []
|
||||||
values = {}
|
values = {}
|
||||||
|
|
||||||
for apply_on in ['Item Code', 'Item Group', 'Brand']:
|
for apply_on in ['Item Code', 'Item Group', 'Brand']:
|
||||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
pricing_rules_all.extend(_get_pricing_rules(apply_on, args, values))
|
||||||
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
|
||||||
break
|
# removing duplicate pricing rule
|
||||||
|
pricing_rules_name = []
|
||||||
|
pricing_rules = []
|
||||||
|
for p in pricing_rules_all:
|
||||||
|
if p['name'] not in pricing_rules_name:
|
||||||
|
pricing_rules_name.append(p['name'])
|
||||||
|
pricing_rules.append(p)
|
||||||
|
|
||||||
rules = []
|
rules = []
|
||||||
|
|
||||||
@@ -323,9 +329,10 @@ def apply_internal_priority(pricing_rules, field_set, args):
|
|||||||
filtered_rules = []
|
filtered_rules = []
|
||||||
for field in field_set:
|
for field in field_set:
|
||||||
if args.get(field):
|
if args.get(field):
|
||||||
# filter function always returns a filter object even if empty
|
for rule in pricing_rules:
|
||||||
# list conversion is necessary to check for an empty result
|
if rule.get(field) == args.get(field):
|
||||||
filtered_rules = list(filter(lambda x: x.get(field)==args.get(field), pricing_rules))
|
filtered_rules = [rule]
|
||||||
|
break
|
||||||
if filtered_rules: break
|
if filtered_rules: break
|
||||||
|
|
||||||
return filtered_rules or pricing_rules
|
return filtered_rules or pricing_rules
|
||||||
|
|||||||
@@ -636,7 +636,8 @@ class PurchaseInvoice(BuyingController):
|
|||||||
item.item_tax_amount / self.conversion_rate)
|
item.item_tax_amount / self.conversion_rate)
|
||||||
}, item=item))
|
}, item=item))
|
||||||
else:
|
else:
|
||||||
cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company)
|
cwip_account = get_asset_account("capital_work_in_progress_account",
|
||||||
|
asset_category=item.asset_category,company=self.company)
|
||||||
|
|
||||||
cwip_account_currency = get_account_currency(cwip_account)
|
cwip_account_currency = get_account_currency(cwip_account)
|
||||||
gl_entries.append(self.get_gl_dict({
|
gl_entries.append(self.get_gl_dict({
|
||||||
|
|||||||
@@ -939,7 +939,8 @@ def make_purchase_invoice(**args):
|
|||||||
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
"cost_center": args.cost_center or "_Test Cost Center - _TC",
|
||||||
"project": args.project,
|
"project": args.project,
|
||||||
"rejected_warehouse": args.rejected_warehouse or "",
|
"rejected_warehouse": args.rejected_warehouse or "",
|
||||||
"rejected_serial_no": args.rejected_serial_no or ""
|
"rejected_serial_no": args.rejected_serial_no or "",
|
||||||
|
"asset_location": args.location or ""
|
||||||
})
|
})
|
||||||
|
|
||||||
if args.get_taxes_and_charges:
|
if args.get_taxes_and_charges:
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ def get_data():
|
|||||||
'Auto Repeat': 'reference_document',
|
'Auto Repeat': 'reference_document',
|
||||||
},
|
},
|
||||||
'internal_links': {
|
'internal_links': {
|
||||||
'Sales Order': ['items', 'sales_order'],
|
'Sales Order': ['items', 'sales_order']
|
||||||
'Delivery Note': ['items', 'delivery_note']
|
|
||||||
},
|
},
|
||||||
'transactions': [
|
'transactions': [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -263,13 +263,14 @@ class Subscription(Document):
|
|||||||
invoice.set_taxes()
|
invoice.set_taxes()
|
||||||
|
|
||||||
# Due date
|
# Due date
|
||||||
invoice.append(
|
if self.days_until_due:
|
||||||
'payment_schedule',
|
invoice.append(
|
||||||
{
|
'payment_schedule',
|
||||||
'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)),
|
{
|
||||||
'invoice_portion': 100
|
'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)),
|
||||||
}
|
'invoice_portion': 100
|
||||||
)
|
}
|
||||||
|
)
|
||||||
|
|
||||||
# Discounts
|
# Discounts
|
||||||
if self.additional_discount_percentage:
|
if self.additional_discount_percentage:
|
||||||
|
|||||||
@@ -210,6 +210,7 @@ class TestSubscription(unittest.TestCase):
|
|||||||
subscription.customer = '_Test Customer'
|
subscription.customer = '_Test Customer'
|
||||||
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1})
|
||||||
subscription.start = '2018-01-01'
|
subscription.start = '2018-01-01'
|
||||||
|
subscription.days_until_due = 1
|
||||||
subscription.insert()
|
subscription.insert()
|
||||||
subscription.process() # generate first invoice
|
subscription.process() # generate first invoice
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
import unittest
|
import unittest
|
||||||
from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template
|
from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template
|
||||||
|
from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity
|
||||||
|
from erpnext.crm.doctype.opportunity.opportunity import make_quotation
|
||||||
|
|
||||||
test_records = frappe.get_test_records('Tax Rule')
|
test_records = frappe.get_test_records('Tax Rule')
|
||||||
|
|
||||||
@@ -144,6 +146,23 @@ class TestTaxRule(unittest.TestCase):
|
|||||||
self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}),
|
self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}),
|
||||||
"_Test Sales Taxes and Charges Template 1 - _TC")
|
"_Test Sales Taxes and Charges Template 1 - _TC")
|
||||||
|
|
||||||
|
def test_taxes_fetch_via_tax_rule(self):
|
||||||
|
make_tax_rule(customer= "_Test Customer", billing_city = "_Test City",
|
||||||
|
sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1)
|
||||||
|
|
||||||
|
# create opportunity for customer
|
||||||
|
opportunity = make_opportunity(with_items=1)
|
||||||
|
|
||||||
|
# make quotation from opportunity
|
||||||
|
quotation = make_quotation(opportunity.name)
|
||||||
|
quotation.save()
|
||||||
|
|
||||||
|
self.assertEqual(quotation.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC")
|
||||||
|
|
||||||
|
# Check if accounts heads and rate fetched are also fetched from tax template or not
|
||||||
|
self.assertTrue(len(quotation.taxes) > 0)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def make_tax_rule(**args):
|
def make_tax_rule(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
|
|||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where company = %s and
|
where company = %s and
|
||||||
party in %s and fiscal_year=%s and credit > 0
|
party in %s and fiscal_year=%s and credit > 0
|
||||||
|
and is_opening = 'No'
|
||||||
""", (company, tuple(suppliers), fiscal_year), as_dict=1)
|
""", (company, tuple(suppliers), fiscal_year), as_dict=1)
|
||||||
|
|
||||||
vouchers = [d.voucher_no for d in entries]
|
vouchers = [d.voucher_no for d in entries]
|
||||||
@@ -139,9 +140,9 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai
|
|||||||
else:
|
else:
|
||||||
tds_amount = _get_tds(net_total, tax_details.rate)
|
tds_amount = _get_tds(net_total, tax_details.rate)
|
||||||
else:
|
else:
|
||||||
supplier_credit_amount = frappe.get_all('Purchase Invoice Item',
|
supplier_credit_amount = frappe.get_all('Purchase Invoice',
|
||||||
fields = ['sum(net_amount)'],
|
fields = ['sum(net_total)'],
|
||||||
filters = {'parent': ('in', vouchers), 'docstatus': 1}, as_list=1)
|
filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
|
||||||
|
|
||||||
supplier_credit_amount = (supplier_credit_amount[0][0]
|
supplier_credit_amount = (supplier_credit_amount[0][0]
|
||||||
if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
|
if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
|
||||||
@@ -192,6 +193,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No
|
|||||||
select distinct voucher_no
|
select distinct voucher_no
|
||||||
from `tabGL Entry`
|
from `tabGL Entry`
|
||||||
where party in %s and %s and debit > 0
|
where party in %s and %s and debit > 0
|
||||||
|
and is_opening = 'No'
|
||||||
""", (tuple(suppliers), condition)) or []
|
""", (tuple(suppliers), condition)) or []
|
||||||
|
|
||||||
def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):
|
def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):
|
||||||
|
|||||||
@@ -101,6 +101,29 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
|||||||
for d in invoices:
|
for d in invoices:
|
||||||
d.cancel()
|
d.cancel()
|
||||||
|
|
||||||
|
def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
|
||||||
|
invoices = []
|
||||||
|
frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
|
||||||
|
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
|
||||||
|
pi.submit()
|
||||||
|
invoices.append(pi)
|
||||||
|
|
||||||
|
# TDS not applied
|
||||||
|
pi = create_purchase_invoice(supplier="Test TDS Supplier2", do_not_apply_tds=True)
|
||||||
|
pi.submit()
|
||||||
|
invoices.append(pi)
|
||||||
|
|
||||||
|
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
|
||||||
|
pi.submit()
|
||||||
|
invoices.append(pi)
|
||||||
|
|
||||||
|
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
|
||||||
|
self.assertEqual(pi.grand_total, 8000)
|
||||||
|
|
||||||
|
# delete invoices to avoid clashing
|
||||||
|
for d in invoices:
|
||||||
|
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.get_doc('Item', {'item_name': 'TDS Item'})
|
||||||
@@ -109,7 +132,7 @@ def create_purchase_invoice(**args):
|
|||||||
pi = frappe.get_doc({
|
pi = frappe.get_doc({
|
||||||
"doctype": "Purchase Invoice",
|
"doctype": "Purchase Invoice",
|
||||||
"posting_date": today(),
|
"posting_date": today(),
|
||||||
"apply_tds": 1,
|
"apply_tds": 0 if args.do_not_apply_tds else 1,
|
||||||
"supplier": args.supplier,
|
"supplier": args.supplier,
|
||||||
"company": '_Test Company',
|
"company": '_Test Company',
|
||||||
"taxes_and_charges": "",
|
"taxes_and_charges": "",
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation {
|
|||||||
check_plaid_status() {
|
check_plaid_status() {
|
||||||
const me = this;
|
const me = this;
|
||||||
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
|
frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
|
||||||
if (r && r.enabled == "1") {
|
if (r && r.enabled === "1") {
|
||||||
me.plaid_status = "active"
|
me.plaid_status = "active"
|
||||||
} else {
|
} else {
|
||||||
me.plaid_status = "inactive"
|
me.plaid_status = "inactive"
|
||||||
@@ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync {
|
|||||||
|
|
||||||
init_config() {
|
init_config() {
|
||||||
const me = this;
|
const me = this;
|
||||||
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration')
|
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
|
||||||
.then(result => {
|
.then(result => {
|
||||||
me.plaid_env = result.plaid_env;
|
me.plaid_env = result.plaid_env;
|
||||||
me.plaid_public_key = result.plaid_public_key;
|
me.client_name = result.client_name;
|
||||||
me.client_name = result.client_name;
|
me.link_token = result.link_token;
|
||||||
me.sync_transactions()
|
me.sync_transactions();
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
sync_transactions() {
|
sync_transactions() {
|
||||||
const me = this;
|
const me = this;
|
||||||
frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (v) => {
|
frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => {
|
||||||
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
|
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
|
||||||
bank: v['bank'],
|
bank: r.bank,
|
||||||
bank_account: me.parent.bank_account,
|
bank_account: me.parent.bank_account,
|
||||||
freeze: true
|
freeze: true
|
||||||
})
|
})
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized")
|
let result_title = (result && result.length > 0)
|
||||||
|
? __("{0} bank transaction(s) created", [result.length])
|
||||||
|
: __("This bank account is already synchronized");
|
||||||
|
|
||||||
let result_msg = `
|
let result_msg = `
|
||||||
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
|
<div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
|
||||||
<h5 class="text-muted">${result_title}</h5>
|
<h5 class="text-muted">${result_title}</h5>
|
||||||
</div>`
|
</div>`
|
||||||
|
|
||||||
this.parent.$main_section.append(result_msg)
|
this.parent.$main_section.append(result_msg)
|
||||||
frappe.show_alert({message:__("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator:'green'});
|
frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' });
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow {
|
|||||||
})
|
})
|
||||||
|
|
||||||
frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments',
|
frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments',
|
||||||
{bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")}
|
{ bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") }
|
||||||
).then((result) => {
|
).then((result) => {
|
||||||
me.make_dialog(result)
|
me.make_dialog(result)
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1064,7 +1064,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
|
|||||||
$(frappe.render_template("pos_item", {
|
$(frappe.render_template("pos_item", {
|
||||||
item_code: escape(obj.name),
|
item_code: escape(obj.name),
|
||||||
item_price: item_price,
|
item_price: item_price,
|
||||||
title: obj.name || obj.item_name,
|
title: obj.name === obj.item_name ? obj.name : obj.item_name,
|
||||||
item_name: obj.name === obj.item_name ? "" : obj.item_name,
|
item_name: obj.name === obj.item_name ? "" : obj.item_name,
|
||||||
item_image: obj.image,
|
item_image: obj.image,
|
||||||
item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj),
|
item_stock: __('Stock Qty') + ": " + me.get_actual_qty(obj),
|
||||||
@@ -1546,7 +1546,7 @@ erpnext.pos.PointOfSale = erpnext.taxes_and_totals.extend({
|
|||||||
$.each(this.frm.doc.items || [], function (i, d) {
|
$.each(this.frm.doc.items || [], function (i, d) {
|
||||||
$(frappe.render_template("pos_bill_item_new", {
|
$(frappe.render_template("pos_bill_item_new", {
|
||||||
item_code: escape(d.item_code),
|
item_code: escape(d.item_code),
|
||||||
title: d.item_code || d.item_name,
|
title: d.item_code === d.item_name ? d.item_code : d.item_name,
|
||||||
item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("<br>" + d.item_name),
|
item_name: (d.item_name === d.item_code || !d.item_name) ? "" : ("<br>" + d.item_name),
|
||||||
qty: d.qty,
|
qty: d.qty,
|
||||||
discount_percentage: d.discount_percentage || 0.0,
|
discount_percentage: d.discount_percentage || 0.0,
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ frappe.query_reports["Accounts Receivable"] = {
|
|||||||
filters: {
|
filters: {
|
||||||
'company': company
|
'company': company
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -617,9 +617,19 @@ class ReceivablePayableReport(object):
|
|||||||
elif party_type_field=="supplier":
|
elif party_type_field=="supplier":
|
||||||
self.add_supplier_filters(conditions, values)
|
self.add_supplier_filters(conditions, values)
|
||||||
|
|
||||||
|
if self.filters.cost_center:
|
||||||
|
self.get_cost_center_conditions(conditions)
|
||||||
|
|
||||||
self.add_accounting_dimensions_filters(conditions, values)
|
self.add_accounting_dimensions_filters(conditions, values)
|
||||||
return " and ".join(conditions), values
|
return " and ".join(conditions), values
|
||||||
|
|
||||||
|
def get_cost_center_conditions(self, conditions):
|
||||||
|
lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"])
|
||||||
|
cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})]
|
||||||
|
|
||||||
|
cost_center_string = '", "'.join(cost_center_list)
|
||||||
|
conditions.append('cost_center in ("{0}")'.format(cost_center_string))
|
||||||
|
|
||||||
def get_order_by_condition(self):
|
def get_order_by_condition(self):
|
||||||
if self.filters.get('group_by_party'):
|
if self.filters.get('group_by_party'):
|
||||||
return "order by party, posting_date"
|
return "order by party, posting_date"
|
||||||
|
|||||||
@@ -3,6 +3,14 @@
|
|||||||
|
|
||||||
frappe.query_reports["Bank Reconciliation Statement"] = {
|
frappe.query_reports["Bank Reconciliation Statement"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
|
{
|
||||||
|
"fieldname":"company",
|
||||||
|
"label": __("Company"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"reqd": 1,
|
||||||
|
"default": frappe.defaults.get_user_default("Company")
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"account",
|
"fieldname":"account",
|
||||||
"label": __("Bank Account"),
|
"label": __("Bank Account"),
|
||||||
@@ -12,11 +20,14 @@ frappe.query_reports["Bank Reconciliation Statement"] = {
|
|||||||
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "",
|
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "",
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"get_query": function() {
|
"get_query": function() {
|
||||||
|
var company = frappe.query_report.get_filter_value('company')
|
||||||
return {
|
return {
|
||||||
"query": "erpnext.controllers.queries.get_account_list",
|
"query": "erpnext.controllers.queries.get_account_list",
|
||||||
"filters": [
|
"filters": [
|
||||||
['Account', 'account_type', 'in', 'Bank, Cash'],
|
['Account', 'account_type', 'in', 'Bank, Cash'],
|
||||||
['Account', 'is_group', '=', 0],
|
['Account', 'is_group', '=', 0],
|
||||||
|
['Account', 'disabled', '=', 0],
|
||||||
|
['Account', 'company', '=', company],
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for(let j=0, k=data.length-1; j<k; j++) { %}
|
{% for(let j=0, k=data.length; j<k; j++) { %}
|
||||||
{%
|
{%
|
||||||
var row = data[j];
|
var row = data[j];
|
||||||
var row_class = data[j].parent_account ? "" : "financial-statements-important";
|
var row_class = data[j].parent_account ? "" : "financial-statements-important";
|
||||||
|
|||||||
@@ -252,13 +252,6 @@ frappe.ui.form.on('Asset', {
|
|||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
|
||||||
available_for_use_date: function(frm) {
|
|
||||||
$.each(frm.doc.finance_books || [], function(i, d) {
|
|
||||||
if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date;
|
|
||||||
});
|
|
||||||
refresh_field("finance_books");
|
|
||||||
},
|
|
||||||
|
|
||||||
is_existing_asset: function(frm) {
|
is_existing_asset: function(frm) {
|
||||||
frm.trigger("toggle_reference_doc");
|
frm.trigger("toggle_reference_doc");
|
||||||
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
|
// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
|
||||||
@@ -437,6 +430,15 @@ frappe.ui.form.on('Asset Finance Book', {
|
|||||||
}
|
}
|
||||||
|
|
||||||
frappe.flags.dont_change_rate = false;
|
frappe.flags.dont_change_rate = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
depreciation_start_date: function(frm, cdt, cdn) {
|
||||||
|
const book = locals[cdt][cdn];
|
||||||
|
if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) {
|
||||||
|
frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`));
|
||||||
|
book.depreciation_start_date = "";
|
||||||
|
frm.refresh_field("finance_books");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ class Asset(AccountsController):
|
|||||||
if not self.available_for_use_date:
|
if not self.available_for_use_date:
|
||||||
frappe.throw(_("Available for use date is required"))
|
frappe.throw(_("Available for use date is required"))
|
||||||
|
|
||||||
|
for d in self.finance_books:
|
||||||
|
if d.depreciation_start_date == self.available_for_use_date:
|
||||||
|
frappe.throw(_("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format(d.idx),
|
||||||
|
title=_("Incorrect Date"))
|
||||||
|
|
||||||
def set_missing_values(self):
|
def set_missing_values(self):
|
||||||
if not self.asset_category:
|
if not self.asset_category:
|
||||||
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category")
|
||||||
@@ -312,7 +317,7 @@ class Asset(AccountsController):
|
|||||||
if not row.depreciation_start_date:
|
if not row.depreciation_start_date:
|
||||||
if not self.available_for_use_date:
|
if not self.available_for_use_date:
|
||||||
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
|
frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx))
|
||||||
row.depreciation_start_date = self.available_for_use_date
|
row.depreciation_start_date = get_last_day(self.available_for_use_date)
|
||||||
|
|
||||||
if not self.is_existing_asset:
|
if not self.is_existing_asset:
|
||||||
self.opening_accumulated_depreciation = 0
|
self.opening_accumulated_depreciation = 0
|
||||||
@@ -465,29 +470,37 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
def validate_make_gl_entry(self):
|
def validate_make_gl_entry(self):
|
||||||
purchase_document = self.get_purchase_document()
|
purchase_document = self.get_purchase_document()
|
||||||
asset_bought_with_invoice = purchase_document == self.purchase_invoice
|
if not purchase_document:
|
||||||
fixed_asset_account, cwip_account = self.get_asset_accounts()
|
|
||||||
cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
|
|
||||||
# check if expense already has been booked in case of cwip was enabled after purchasing asset
|
|
||||||
expense_booked = False
|
|
||||||
cwip_booked = False
|
|
||||||
|
|
||||||
if asset_bought_with_invoice:
|
|
||||||
expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
|
|
||||||
(purchase_document, fixed_asset_account), as_dict=1)
|
|
||||||
else:
|
|
||||||
cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""",
|
|
||||||
(purchase_document, cwip_account), as_dict=1)
|
|
||||||
|
|
||||||
if cwip_enabled and (expense_booked or not cwip_booked):
|
|
||||||
# if expense has already booked from invoice or cwip is booked from receipt
|
|
||||||
return False
|
return False
|
||||||
elif not cwip_enabled and (not expense_booked or cwip_booked):
|
|
||||||
# if cwip is disabled but expense hasn't been booked yet
|
asset_bought_with_invoice = (purchase_document == self.purchase_invoice)
|
||||||
return True
|
fixed_asset_account = self.get_fixed_asset_account()
|
||||||
elif cwip_enabled:
|
|
||||||
# default condition
|
cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
|
||||||
return True
|
cwip_account = self.get_cwip_account(cwip_enabled=cwip_enabled)
|
||||||
|
|
||||||
|
query = """SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s"""
|
||||||
|
if asset_bought_with_invoice:
|
||||||
|
# with invoice purchase either expense or cwip has been booked
|
||||||
|
expense_booked = frappe.db.sql(query, (purchase_document, fixed_asset_account), as_dict=1)
|
||||||
|
if expense_booked:
|
||||||
|
# if expense is already booked from invoice then do not make gl entries regardless of cwip enabled/disabled
|
||||||
|
return False
|
||||||
|
|
||||||
|
cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
|
||||||
|
if cwip_booked:
|
||||||
|
# if cwip is booked from invoice then make gl entries regardless of cwip enabled/disabled
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
# with receipt purchase either cwip has been booked or no entries have been made
|
||||||
|
if not cwip_account:
|
||||||
|
# if cwip account isn't available do not make gl entries
|
||||||
|
return False
|
||||||
|
|
||||||
|
cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
|
||||||
|
# if cwip is not booked from receipt then do not make gl entries
|
||||||
|
# if cwip is booked from receipt then make gl entries
|
||||||
|
return cwip_booked
|
||||||
|
|
||||||
def get_purchase_document(self):
|
def get_purchase_document(self):
|
||||||
asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')
|
asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock')
|
||||||
@@ -495,20 +508,25 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
return purchase_document
|
return purchase_document
|
||||||
|
|
||||||
def get_asset_accounts(self):
|
def get_fixed_asset_account(self):
|
||||||
fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name,
|
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
|
||||||
asset_category = self.asset_category, company = self.company)
|
|
||||||
|
|
||||||
cwip_account = get_asset_account("capital_work_in_progress_account",
|
def get_cwip_account(self, cwip_enabled=False):
|
||||||
self.name, self.asset_category, self.company)
|
cwip_account = None
|
||||||
|
try:
|
||||||
|
cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company)
|
||||||
|
except:
|
||||||
|
# if no cwip account found in category or company and "cwip is enabled" then raise else silently pass
|
||||||
|
if cwip_enabled:
|
||||||
|
raise
|
||||||
|
|
||||||
return fixed_asset_account, cwip_account
|
return cwip_account
|
||||||
|
|
||||||
def make_gl_entries(self):
|
def make_gl_entries(self):
|
||||||
gl_entries = []
|
gl_entries = []
|
||||||
|
|
||||||
purchase_document = self.get_purchase_document()
|
purchase_document = self.get_purchase_document()
|
||||||
fixed_asset_account, cwip_account = self.get_asset_accounts()
|
fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account()
|
||||||
|
|
||||||
if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()):
|
if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()):
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad
|
|||||||
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
|
from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset
|
||||||
from erpnext.assets.doctype.asset.asset import make_sales_invoice
|
from erpnext.assets.doctype.asset.asset import make_sales_invoice
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice
|
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice
|
||||||
|
|
||||||
class TestAsset(unittest.TestCase):
|
class TestAsset(unittest.TestCase):
|
||||||
@@ -374,19 +375,18 @@ class TestAsset(unittest.TestCase):
|
|||||||
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
|
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
|
||||||
asset = frappe.get_doc('Asset', asset_name)
|
asset = frappe.get_doc('Asset', asset_name)
|
||||||
asset.calculate_depreciation = 1
|
asset.calculate_depreciation = 1
|
||||||
asset.available_for_use_date = nowdate()
|
asset.available_for_use_date = '2020-01-01'
|
||||||
asset.purchase_date = nowdate()
|
asset.purchase_date = '2020-01-01'
|
||||||
asset.append("finance_books", {
|
asset.append("finance_books", {
|
||||||
"expected_value_after_useful_life": 10000,
|
"expected_value_after_useful_life": 10000,
|
||||||
"depreciation_method": "Straight Line",
|
"depreciation_method": "Straight Line",
|
||||||
"total_number_of_depreciations": 3,
|
"total_number_of_depreciations": 10,
|
||||||
"frequency_of_depreciation": 10,
|
"frequency_of_depreciation": 1
|
||||||
"depreciation_start_date": nowdate()
|
|
||||||
})
|
})
|
||||||
asset.insert()
|
asset.insert()
|
||||||
asset.submit()
|
asset.submit()
|
||||||
|
|
||||||
post_depreciation_entries(date=add_months(nowdate(), 10))
|
post_depreciation_entries(date=add_months('2020-01-01', 4))
|
||||||
|
|
||||||
scrap_asset(asset.name)
|
scrap_asset(asset.name)
|
||||||
|
|
||||||
@@ -395,9 +395,9 @@ class TestAsset(unittest.TestCase):
|
|||||||
self.assertTrue(asset.journal_entry_for_scrap)
|
self.assertTrue(asset.journal_entry_for_scrap)
|
||||||
|
|
||||||
expected_gle = (
|
expected_gle = (
|
||||||
("_Test Accumulated Depreciations - _TC", 30000.0, 0.0),
|
("_Test Accumulated Depreciations - _TC", 36000.0, 0.0),
|
||||||
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
("_Test Fixed Asset - _TC", 0.0, 100000.0),
|
||||||
("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0)
|
("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0)
|
||||||
)
|
)
|
||||||
|
|
||||||
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
||||||
@@ -469,8 +469,7 @@ class TestAsset(unittest.TestCase):
|
|||||||
"expected_value_after_useful_life": 10000,
|
"expected_value_after_useful_life": 10000,
|
||||||
"depreciation_method": "Straight Line",
|
"depreciation_method": "Straight Line",
|
||||||
"total_number_of_depreciations": 3,
|
"total_number_of_depreciations": 3,
|
||||||
"frequency_of_depreciation": 10,
|
"frequency_of_depreciation": 10
|
||||||
"depreciation_start_date": "2020-06-06"
|
|
||||||
})
|
})
|
||||||
asset.insert()
|
asset.insert()
|
||||||
accumulated_depreciation_after_full_schedule = \
|
accumulated_depreciation_after_full_schedule = \
|
||||||
@@ -563,81 +562,6 @@ class TestAsset(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(gle, expected_gle)
|
self.assertEqual(gle, expected_gle)
|
||||||
|
|
||||||
def test_gle_with_cwip_toggling(self):
|
|
||||||
# TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset
|
|
||||||
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
|
|
||||||
|
|
||||||
pr = make_purchase_receipt(item_code="Macbook Pro",
|
|
||||||
qty=1, rate=5000, do_not_submit=True, location="Test Location")
|
|
||||||
pr.set('taxes', [{
|
|
||||||
'category': 'Total',
|
|
||||||
'add_deduct_tax': 'Add',
|
|
||||||
'charge_type': 'On Net Total',
|
|
||||||
'account_head': '_Test Account Service Tax - _TC',
|
|
||||||
'description': '_Test Account Service Tax',
|
|
||||||
'cost_center': 'Main - _TC',
|
|
||||||
'rate': 5.0
|
|
||||||
}, {
|
|
||||||
'category': 'Valuation and Total',
|
|
||||||
'add_deduct_tax': 'Add',
|
|
||||||
'charge_type': 'On Net Total',
|
|
||||||
'account_head': '_Test Account Shipping Charges - _TC',
|
|
||||||
'description': '_Test Account Shipping Charges',
|
|
||||||
'cost_center': 'Main - _TC',
|
|
||||||
'rate': 5.0
|
|
||||||
}])
|
|
||||||
pr.submit()
|
|
||||||
expected_gle = (
|
|
||||||
("Asset Received But Not Billed - _TC", 0.0, 5250.0),
|
|
||||||
("CWIP Account - _TC", 5250.0, 0.0)
|
|
||||||
)
|
|
||||||
pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
|
||||||
where voucher_type='Purchase Receipt' and voucher_no = %s
|
|
||||||
order by account""", pr.name)
|
|
||||||
self.assertEqual(pr_gle, expected_gle)
|
|
||||||
|
|
||||||
pi = make_invoice(pr.name)
|
|
||||||
pi.submit()
|
|
||||||
expected_gle = (
|
|
||||||
("_Test Account Service Tax - _TC", 250.0, 0.0),
|
|
||||||
("_Test Account Shipping Charges - _TC", 250.0, 0.0),
|
|
||||||
("Asset Received But Not Billed - _TC", 5250.0, 0.0),
|
|
||||||
("Creditors - _TC", 0.0, 5500.0),
|
|
||||||
("Expenses Included In Asset Valuation - _TC", 0.0, 250.0),
|
|
||||||
)
|
|
||||||
pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
|
||||||
where voucher_type='Purchase Invoice' and voucher_no = %s
|
|
||||||
order by account""", pi.name)
|
|
||||||
self.assertEqual(pi_gle, expected_gle)
|
|
||||||
|
|
||||||
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
|
|
||||||
asset_doc = frappe.get_doc('Asset', asset)
|
|
||||||
month_end_date = get_last_day(nowdate())
|
|
||||||
asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
|
|
||||||
self.assertEqual(asset_doc.gross_purchase_amount, 5250.0)
|
|
||||||
asset_doc.append("finance_books", {
|
|
||||||
"expected_value_after_useful_life": 200,
|
|
||||||
"depreciation_method": "Straight Line",
|
|
||||||
"total_number_of_depreciations": 3,
|
|
||||||
"frequency_of_depreciation": 10,
|
|
||||||
"depreciation_start_date": month_end_date
|
|
||||||
})
|
|
||||||
|
|
||||||
# disable cwip and try submitting
|
|
||||||
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
|
|
||||||
asset_doc.submit()
|
|
||||||
# asset should have gl entries even if cwip is disabled
|
|
||||||
expected_gle = (
|
|
||||||
("_Test Fixed Asset - _TC", 5250.0, 0.0),
|
|
||||||
("CWIP Account - _TC", 0.0, 5250.0)
|
|
||||||
)
|
|
||||||
gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
|
|
||||||
where voucher_type='Asset' and voucher_no = %s
|
|
||||||
order by account""", asset_doc.name)
|
|
||||||
self.assertEqual(gle, expected_gle)
|
|
||||||
|
|
||||||
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
|
|
||||||
|
|
||||||
def test_expense_head(self):
|
def test_expense_head(self):
|
||||||
pr = make_purchase_receipt(item_code="Macbook Pro",
|
pr = make_purchase_receipt(item_code="Macbook Pro",
|
||||||
qty=2, rate=200000.0, location="Test Location")
|
qty=2, rate=200000.0, location="Test Location")
|
||||||
@@ -646,6 +570,74 @@ class TestAsset(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
|
self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account)
|
||||||
|
|
||||||
|
def test_asset_cwip_toggling_cases(self):
|
||||||
|
cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting")
|
||||||
|
name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"])
|
||||||
|
cwip_acc = "CWIP Account - _TC"
|
||||||
|
|
||||||
|
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
|
||||||
|
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "")
|
||||||
|
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", "")
|
||||||
|
|
||||||
|
# case 0 -- PI with cwip disable, Asset with cwip disabled, No cwip account set
|
||||||
|
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
|
||||||
|
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
|
||||||
|
asset_doc = frappe.get_doc('Asset', asset)
|
||||||
|
asset_doc.available_for_use_date = nowdate()
|
||||||
|
asset_doc.calculate_depreciation = 0
|
||||||
|
asset_doc.submit()
|
||||||
|
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
|
||||||
|
self.assertFalse(gle)
|
||||||
|
|
||||||
|
# case 1 -- PR with cwip disabled, Asset with cwip enabled
|
||||||
|
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location")
|
||||||
|
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
|
||||||
|
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
|
||||||
|
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
|
||||||
|
asset_doc = frappe.get_doc('Asset', asset)
|
||||||
|
asset_doc.available_for_use_date = nowdate()
|
||||||
|
asset_doc.calculate_depreciation = 0
|
||||||
|
asset_doc.submit()
|
||||||
|
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
|
||||||
|
self.assertFalse(gle)
|
||||||
|
|
||||||
|
# case 2 -- PR with cwip enabled, Asset with cwip disabled
|
||||||
|
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location")
|
||||||
|
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
|
||||||
|
asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name')
|
||||||
|
asset_doc = frappe.get_doc('Asset', asset)
|
||||||
|
asset_doc.available_for_use_date = nowdate()
|
||||||
|
asset_doc.calculate_depreciation = 0
|
||||||
|
asset_doc.submit()
|
||||||
|
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
|
||||||
|
self.assertTrue(gle)
|
||||||
|
|
||||||
|
# case 3 -- PI with cwip disabled, Asset with cwip enabled
|
||||||
|
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
|
||||||
|
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1)
|
||||||
|
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
|
||||||
|
asset_doc = frappe.get_doc('Asset', asset)
|
||||||
|
asset_doc.available_for_use_date = nowdate()
|
||||||
|
asset_doc.calculate_depreciation = 0
|
||||||
|
asset_doc.submit()
|
||||||
|
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
|
||||||
|
self.assertFalse(gle)
|
||||||
|
|
||||||
|
# case 4 -- PI with cwip enabled, Asset with cwip disabled
|
||||||
|
pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1)
|
||||||
|
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0)
|
||||||
|
asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name')
|
||||||
|
asset_doc = frappe.get_doc('Asset', asset)
|
||||||
|
asset_doc.available_for_use_date = nowdate()
|
||||||
|
asset_doc.calculate_depreciation = 0
|
||||||
|
asset_doc.submit()
|
||||||
|
gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name)
|
||||||
|
self.assertTrue(gle)
|
||||||
|
|
||||||
|
frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip)
|
||||||
|
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
|
||||||
|
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
|
||||||
|
|
||||||
def create_asset_data():
|
def create_asset_data():
|
||||||
if not frappe.db.exists("Asset Category", "Computers"):
|
if not frappe.db.exists("Asset Category", "Computers"):
|
||||||
create_asset_category()
|
create_asset_category()
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint, get_link_to_form
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
|
||||||
class AssetCategory(Document):
|
class AssetCategory(Document):
|
||||||
@@ -13,6 +13,7 @@ class AssetCategory(Document):
|
|||||||
self.validate_finance_books()
|
self.validate_finance_books()
|
||||||
self.validate_account_types()
|
self.validate_account_types()
|
||||||
self.validate_account_currency()
|
self.validate_account_currency()
|
||||||
|
self.valide_cwip_account()
|
||||||
|
|
||||||
def validate_finance_books(self):
|
def validate_finance_books(self):
|
||||||
for d in self.finance_books:
|
for d in self.finance_books:
|
||||||
@@ -59,6 +60,21 @@ class AssetCategory(Document):
|
|||||||
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
|
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
|
||||||
title=_("Invalid Account"))
|
title=_("Invalid Account"))
|
||||||
|
|
||||||
|
def valide_cwip_account(self):
|
||||||
|
if self.enable_cwip_accounting:
|
||||||
|
missing_cwip_accounts_for_company = []
|
||||||
|
for d in self.accounts:
|
||||||
|
if (not d.capital_work_in_progress_account and
|
||||||
|
not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")):
|
||||||
|
missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name))
|
||||||
|
|
||||||
|
if missing_cwip_accounts_for_company:
|
||||||
|
msg = _("""To enable Capital Work in Progress Accounting, """)
|
||||||
|
msg += _("""you must select Capital Work in Progress Account in accounts table""")
|
||||||
|
msg += "<br><br>"
|
||||||
|
msg += _("You can also set default CWIP account in Company {}").format(", ".join(missing_cwip_accounts_for_company))
|
||||||
|
frappe.throw(msg, title=_("Missing Account"))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None):
|
def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None):
|
||||||
|
|||||||
@@ -27,3 +27,21 @@ class TestAssetCategory(unittest.TestCase):
|
|||||||
except frappe.DuplicateEntryError:
|
except frappe.DuplicateEntryError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def test_cwip_accounting(self):
|
||||||
|
company_cwip_acc = frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account")
|
||||||
|
frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "")
|
||||||
|
|
||||||
|
asset_category = frappe.new_doc("Asset Category")
|
||||||
|
asset_category.asset_category_name = "Computers"
|
||||||
|
asset_category.enable_cwip_accounting = 1
|
||||||
|
|
||||||
|
asset_category.total_number_of_depreciations = 3
|
||||||
|
asset_category.frequency_of_depreciation = 3
|
||||||
|
asset_category.append("accounts", {
|
||||||
|
"company_name": "_Test Company",
|
||||||
|
"fixed_asset_account": "_Test Fixed Asset - _TC",
|
||||||
|
"accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC",
|
||||||
|
"depreciation_expense_account": "_Test Depreciations - _TC"
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertRaises(frappe.ValidationError, asset_category.insert)
|
||||||
@@ -1,347 +1,99 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 0,
|
|
||||||
"allow_rename": 0,
|
|
||||||
"beta": 0,
|
|
||||||
"creation": "2018-05-08 14:44:37.095570",
|
"creation": "2018-05-08 14:44:37.095570",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "",
|
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"finance_book",
|
||||||
|
"depreciation_method",
|
||||||
|
"total_number_of_depreciations",
|
||||||
|
"column_break_5",
|
||||||
|
"frequency_of_depreciation",
|
||||||
|
"depreciation_start_date",
|
||||||
|
"expected_value_after_useful_life",
|
||||||
|
"value_after_depreciation",
|
||||||
|
"rate_of_depreciation"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "",
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "finance_book",
|
"fieldname": "finance_book",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Finance Book",
|
"label": "Finance Book",
|
||||||
"length": 0,
|
"options": "Finance Book"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Finance Book",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "depreciation_method",
|
"fieldname": "depreciation_method",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Depreciation Method",
|
"label": "Depreciation Method",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
|
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
|
||||||
"permlevel": 0,
|
"reqd": 1
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "total_number_of_depreciations",
|
"fieldname": "total_number_of_depreciations",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Total Number of Depreciations",
|
"label": "Total Number of Depreciations",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "column_break_5",
|
"fieldname": "column_break_5",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "frequency_of_depreciation",
|
"fieldname": "frequency_of_depreciation",
|
||||||
"fieldtype": "Int",
|
"fieldtype": "Int",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Frequency of Depreciation (Months)",
|
"label": "Frequency of Depreciation (Months)",
|
||||||
"length": 0,
|
"reqd": 1
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:parent.doctype == 'Asset'",
|
"depends_on": "eval:parent.doctype == 'Asset'",
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "depreciation_start_date",
|
"fieldname": "depreciation_start_date",
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
"label": "Depreciation Posting Date",
|
||||||
"label": "Depreciation Start Date",
|
"reqd": 1
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "eval:parent.doctype == 'Asset'",
|
"depends_on": "eval:parent.doctype == 'Asset'",
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "expected_value_after_useful_life",
|
"fieldname": "expected_value_after_useful_life",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Expected Value After Useful Life",
|
"label": "Expected Value After Useful Life",
|
||||||
"length": 0,
|
"options": "Company:company:default_currency"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Company:company:default_currency",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "value_after_depreciation",
|
"fieldname": "value_after_depreciation",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"hidden": 1,
|
"hidden": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Value After Depreciation",
|
"label": "Value After Depreciation",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
"print_hide_if_no_value": 0,
|
"read_only": 1
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
|
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
|
||||||
"description": "In Percentage",
|
"description": "In Percentage",
|
||||||
"fetch_if_empty": 0,
|
|
||||||
"fieldname": "rate_of_depreciation",
|
"fieldname": "rate_of_depreciation",
|
||||||
"fieldtype": "Percent",
|
"fieldtype": "Percent",
|
||||||
"hidden": 0,
|
"label": "Rate of Depreciation"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Rate of Depreciation",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"hide_toolbar": 0,
|
|
||||||
"idx": 0,
|
|
||||||
"in_create": 0,
|
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"max_attachments": 0,
|
"links": [],
|
||||||
"modified": "2019-04-09 19:45:14.523488",
|
"modified": "2020-09-16 12:11:30.631788",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Assets",
|
"module": "Assets",
|
||||||
"name": "Asset Finance Book",
|
"name": "Asset Finance Book",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"quick_entry": 1,
|
"quick_entry": 1,
|
||||||
"read_only": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
||||||
@@ -32,8 +32,7 @@ class TestAssetMovement(unittest.TestCase):
|
|||||||
"next_depreciation_date": "2020-12-31",
|
"next_depreciation_date": "2020-12-31",
|
||||||
"depreciation_method": "Straight Line",
|
"depreciation_method": "Straight Line",
|
||||||
"total_number_of_depreciations": 3,
|
"total_number_of_depreciations": 3,
|
||||||
"frequency_of_depreciation": 10,
|
"frequency_of_depreciation": 10
|
||||||
"depreciation_start_date": "2020-06-06"
|
|
||||||
})
|
})
|
||||||
|
|
||||||
if asset.docstatus == 0:
|
if asset.docstatus == 0:
|
||||||
@@ -82,8 +81,7 @@ class TestAssetMovement(unittest.TestCase):
|
|||||||
"next_depreciation_date": "2020-12-31",
|
"next_depreciation_date": "2020-12-31",
|
||||||
"depreciation_method": "Straight Line",
|
"depreciation_method": "Straight Line",
|
||||||
"total_number_of_depreciations": 3,
|
"total_number_of_depreciations": 3,
|
||||||
"frequency_of_depreciation": 10,
|
"frequency_of_depreciation": 10
|
||||||
"depreciation_start_date": "2020-06-06"
|
|
||||||
})
|
})
|
||||||
if asset.docstatus == 0:
|
if asset.docstatus == 0:
|
||||||
asset.submit()
|
asset.submit()
|
||||||
|
|||||||
@@ -202,6 +202,91 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name)
|
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name)
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
|
def test_update_child_with_tax_template(self):
|
||||||
|
"""
|
||||||
|
Test Action: Create a PO with one item having its tax account head already in the PO.
|
||||||
|
Add the same item + new item with tax template via Update Items.
|
||||||
|
Expected result: First Item's tax row is updated. New tax row is added for second Item.
|
||||||
|
"""
|
||||||
|
if not frappe.db.exists("Item", "Test Item with Tax"):
|
||||||
|
make_item("Test Item with Tax", {
|
||||||
|
'is_stock_item': 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'Item Tax Template',
|
||||||
|
'title': 'Test Update Items Template',
|
||||||
|
'company': '_Test Company',
|
||||||
|
'taxes': [
|
||||||
|
{
|
||||||
|
'tax_type': "_Test Account Service Tax - _TC",
|
||||||
|
'tax_rate': 10,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
|
||||||
|
|
||||||
|
new_item_with_tax.append("taxes", {
|
||||||
|
"item_tax_template": "Test Update Items Template",
|
||||||
|
"valid_from": nowdate()
|
||||||
|
})
|
||||||
|
new_item_with_tax.save()
|
||||||
|
|
||||||
|
tax_template = "_Test Account Excise Duty @ 10"
|
||||||
|
item = "_Test Item Home Desktop 100"
|
||||||
|
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
|
||||||
|
item_doc = frappe.get_doc("Item", item)
|
||||||
|
item_doc.append("taxes", {
|
||||||
|
"item_tax_template": tax_template,
|
||||||
|
"valid_from": nowdate()
|
||||||
|
})
|
||||||
|
item_doc.save()
|
||||||
|
else:
|
||||||
|
# update valid from
|
||||||
|
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
|
||||||
|
where parent = %(item)s and item_tax_template = %(tax)s""",
|
||||||
|
{"item": item, "tax": tax_template})
|
||||||
|
|
||||||
|
po = create_purchase_order(item_code=item, qty=1, do_not_save=1)
|
||||||
|
|
||||||
|
po.append("taxes", {
|
||||||
|
"account_head": "_Test Account Excise Duty - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Excise Duty",
|
||||||
|
"doctype": "Purchase Taxes and Charges",
|
||||||
|
"rate": 10
|
||||||
|
})
|
||||||
|
po.insert()
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
self.assertEqual(po.taxes[0].tax_amount, 50)
|
||||||
|
self.assertEqual(po.taxes[0].total, 550)
|
||||||
|
|
||||||
|
items = json.dumps([
|
||||||
|
{'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name},
|
||||||
|
{'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO
|
||||||
|
{'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
|
||||||
|
])
|
||||||
|
update_child_qty_rate('Purchase Order', items, po.name)
|
||||||
|
|
||||||
|
po.reload()
|
||||||
|
self.assertEqual(po.taxes[0].tax_amount, 70)
|
||||||
|
self.assertEqual(po.taxes[0].total, 770)
|
||||||
|
self.assertEqual(po.taxes[1].account_head, "_Test Account Service Tax - _TC")
|
||||||
|
self.assertEqual(po.taxes[1].tax_amount, 70)
|
||||||
|
self.assertEqual(po.taxes[1].total, 840)
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
|
||||||
|
where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
|
||||||
|
po.cancel()
|
||||||
|
po.delete()
|
||||||
|
new_item_with_tax.delete()
|
||||||
|
frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
|
||||||
|
|
||||||
def test_update_qty(self):
|
def test_update_qty(self):
|
||||||
po = create_purchase_order()
|
po = create_purchase_order()
|
||||||
|
|
||||||
@@ -733,6 +818,59 @@ class TestPurchaseOrder(unittest.TestCase):
|
|||||||
|
|
||||||
update_backflush_based_on("BOM")
|
update_backflush_based_on("BOM")
|
||||||
|
|
||||||
|
def test_supplied_qty_against_subcontracted_po(self):
|
||||||
|
item_code = "_Test Subcontracted FG Item 5"
|
||||||
|
make_item('Sub Contracted Raw Material 4', {
|
||||||
|
'is_stock_item': 1,
|
||||||
|
'is_sub_contracted_item': 1
|
||||||
|
})
|
||||||
|
|
||||||
|
make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
|
||||||
|
|
||||||
|
update_backflush_based_on("Material Transferred for Subcontract")
|
||||||
|
|
||||||
|
order_qty = 250
|
||||||
|
po = create_purchase_order(item_code=item_code, qty=order_qty,
|
||||||
|
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", do_not_save=True)
|
||||||
|
|
||||||
|
# Add same subcontracted items multiple times
|
||||||
|
po.append("items", {
|
||||||
|
"item_code": item_code,
|
||||||
|
"qty": order_qty,
|
||||||
|
"schedule_date": add_days(nowdate(), 1),
|
||||||
|
"warehouse": "_Test Warehouse - _TC"
|
||||||
|
})
|
||||||
|
|
||||||
|
po.set_missing_values()
|
||||||
|
po.submit()
|
||||||
|
|
||||||
|
# Material receipt entry for the raw materials which will be send to supplier
|
||||||
|
make_stock_entry(target="_Test Warehouse - _TC",
|
||||||
|
item_code = "Sub Contracted Raw Material 4", qty=500, basic_rate=100)
|
||||||
|
|
||||||
|
rm_items = [
|
||||||
|
{
|
||||||
|
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
|
||||||
|
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 4","item_name":"_Test Item",
|
||||||
|
"qty":250,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[1].name
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
# Raw Materials transfer entry from stores to supplier's warehouse
|
||||||
|
rm_item_string = json.dumps(rm_items)
|
||||||
|
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
|
||||||
|
se.submit()
|
||||||
|
|
||||||
|
po_doc = frappe.get_doc("Purchase Order", po.name)
|
||||||
|
for row in po_doc.supplied_items:
|
||||||
|
# Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
|
||||||
|
self.assertEqual(row.supplied_qty, 250.0)
|
||||||
|
|
||||||
|
update_backflush_based_on("BOM")
|
||||||
|
|
||||||
def test_advance_payment_entry_unlink_against_purchase_order(self):
|
def test_advance_payment_entry_unlink_against_purchase_order(self):
|
||||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||||
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ def get_conditions(filters):
|
|||||||
conditions = ""
|
conditions = ""
|
||||||
|
|
||||||
if filters.get("company"):
|
if filters.get("company"):
|
||||||
conditions += " AND par.company=%s" % frappe.db.escape(filters.get('company'))
|
conditions += " AND parent.company=%s" % frappe.db.escape(filters.get('company'))
|
||||||
|
|
||||||
if filters.get("cost_center") or filters.get("project"):
|
if filters.get("cost_center") or filters.get("project"):
|
||||||
conditions += """
|
conditions += """
|
||||||
@@ -151,10 +151,10 @@ def get_conditions(filters):
|
|||||||
""" % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
|
""" % (frappe.db.escape(filters.get('cost_center')), frappe.db.escape(filters.get('project')))
|
||||||
|
|
||||||
if filters.get("from_date"):
|
if filters.get("from_date"):
|
||||||
conditions += " AND par.transaction_date>='%s'" % filters.get('from_date')
|
conditions += " AND parent.transaction_date>='%s'" % filters.get('from_date')
|
||||||
|
|
||||||
if filters.get("to_date"):
|
if filters.get("to_date"):
|
||||||
conditions += " AND par.transaction_date<='%s'" % filters.get('to_date')
|
conditions += " AND parent.transaction_date<='%s'" % filters.get('to_date')
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
def get_data(filters):
|
def get_data(filters):
|
||||||
@@ -198,21 +198,23 @@ def get_mapped_mr_details(conditions):
|
|||||||
mr_records = {}
|
mr_records = {}
|
||||||
mr_details = frappe.db.sql("""
|
mr_details = frappe.db.sql("""
|
||||||
SELECT
|
SELECT
|
||||||
par.transaction_date,
|
parent.transaction_date,
|
||||||
par.per_ordered,
|
parent.per_ordered,
|
||||||
par.owner,
|
parent.owner,
|
||||||
child.name,
|
child.name,
|
||||||
child.parent,
|
child.parent,
|
||||||
child.amount,
|
child.amount,
|
||||||
child.qty,
|
child.qty,
|
||||||
child.item_code,
|
child.item_code,
|
||||||
child.uom,
|
child.uom,
|
||||||
par.status
|
parent.status,
|
||||||
FROM `tabMaterial Request` par, `tabMaterial Request Item` child
|
child.project,
|
||||||
|
child.cost_center
|
||||||
|
FROM `tabMaterial Request` parent, `tabMaterial Request Item` child
|
||||||
WHERE
|
WHERE
|
||||||
par.per_ordered>=0
|
parent.per_ordered>=0
|
||||||
AND par.name=child.parent
|
AND parent.name=child.parent
|
||||||
AND par.docstatus=1
|
AND parent.docstatus=1
|
||||||
{conditions}
|
{conditions}
|
||||||
""".format(conditions=conditions), as_dict=1) #nosec
|
""".format(conditions=conditions), as_dict=1) #nosec
|
||||||
|
|
||||||
@@ -232,7 +234,9 @@ def get_mapped_mr_details(conditions):
|
|||||||
status=record.status,
|
status=record.status,
|
||||||
actual_cost=0,
|
actual_cost=0,
|
||||||
purchase_order_amt=0,
|
purchase_order_amt=0,
|
||||||
purchase_order_amt_in_company_currency=0
|
purchase_order_amt_in_company_currency=0,
|
||||||
|
project = record.project,
|
||||||
|
cost_center = record.cost_center
|
||||||
)
|
)
|
||||||
procurement_record_against_mr.append(procurement_record_details)
|
procurement_record_against_mr.append(procurement_record_details)
|
||||||
return mr_records, procurement_record_against_mr
|
return mr_records, procurement_record_against_mr
|
||||||
@@ -280,16 +284,16 @@ def get_po_entries(conditions):
|
|||||||
child.amount,
|
child.amount,
|
||||||
child.base_amount,
|
child.base_amount,
|
||||||
child.schedule_date,
|
child.schedule_date,
|
||||||
par.transaction_date,
|
parent.transaction_date,
|
||||||
par.supplier,
|
parent.supplier,
|
||||||
par.status,
|
parent.status,
|
||||||
par.owner
|
parent.owner
|
||||||
FROM `tabPurchase Order` par, `tabPurchase Order Item` child
|
FROM `tabPurchase Order` parent, `tabPurchase Order Item` child
|
||||||
WHERE
|
WHERE
|
||||||
par.docstatus = 1
|
parent.docstatus = 1
|
||||||
AND par.name = child.parent
|
AND parent.name = child.parent
|
||||||
AND par.status not in ("Closed","Completed","Cancelled")
|
AND parent.status not in ("Closed","Completed","Cancelled")
|
||||||
{conditions}
|
{conditions}
|
||||||
GROUP BY
|
GROUP BY
|
||||||
par.name, child.item_code
|
parent.name, child.item_code
|
||||||
""".format(conditions=conditions), as_dict=1) #nosec
|
""".format(conditions=conditions), as_dict=1) #nosec
|
||||||
51
erpnext/change_log/v12/v12_13_0.md
Normal file
51
erpnext/change_log/v12/v12_13_0.md
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
## ERPNext v12.13.0 Release Note
|
||||||
|
|
||||||
|
### Fixes and Enhancements
|
||||||
|
|
||||||
|
- Warehouse address filtered based on warehouse ([#23381](https://github.com/frappe/erpnext/pull/23381))
|
||||||
|
- COGS validation in the purchase receipt ([#23536](https://github.com/frappe/erpnext/pull/23536))
|
||||||
|
- Item Tax Updating via `Update Items` in SO/PO ([#23338](https://github.com/frappe/erpnext/pull/23338))
|
||||||
|
- Pricing rule selector is wrong ([#22328](https://github.com/frappe/erpnext/pull/22328))
|
||||||
|
- Adding filters validation Batch-Wise Balance History ([#23396](https://github.com/frappe/erpnext/pull/23396))
|
||||||
|
- Add company and correct filter in bank statement reconciliation report filters ([#23618](https://github.com/frappe/erpnext/pull/23618))
|
||||||
|
- Incorrect consumed qty if raw material with batch ([#23389](https://github.com/frappe/erpnext/pull/23389))
|
||||||
|
- Balance serial nos in stock ledger report ([#23520](https://github.com/frappe/erpnext/pull/23520))
|
||||||
|
- Use Plaid's new API (v12) ([#23317](https://github.com/frappe/erpnext/pull/23317))
|
||||||
|
- Cost Center filter in accounts receivable and payable report ([#23356](https://github.com/frappe/erpnext/pull/23356))
|
||||||
|
- Incorrect operation time calculation for batch size ([#23480](https://github.com/frappe/erpnext/pull/23480))
|
||||||
|
- Serial no field is blank in stock reconciliation ([#23648](https://github.com/frappe/erpnext/pull/23648))
|
||||||
|
- Manually set serial nos override with current available serial nos ([#23657](https://github.com/frappe/erpnext/pull/23657))
|
||||||
|
- Can't save item price after adding child table ([#23594](https://github.com/frappe/erpnext/pull/23594))
|
||||||
|
- Naming series - cannot reset current value to zero ([#23505](https://github.com/frappe/erpnext/pull/23505))
|
||||||
|
- TDS calculation, skip invoices with "Apply Tax Withholding Amount" has disabled ([#23463](https://github.com/frappe/erpnext/pull/23463))
|
||||||
|
- Taxes not getting fetched from Opportunity to Quotation ([#23354](https://github.com/frappe/erpnext/pull/23354))
|
||||||
|
- Stock reconciliation, incorrect serial nos fetched in the current serial no field ([#23366](https://github.com/frappe/erpnext/pull/23366))
|
||||||
|
- Cannot create asset if cwip disabled and account not set ([#23584](https://github.com/frappe/erpnext/pull/23584))
|
||||||
|
- Leave application status fix ([#23411](https://github.com/frappe/erpnext/pull/23411))
|
||||||
|
- Not able to do overproduction ([#23600](https://github.com/frappe/erpnext/pull/23600))
|
||||||
|
- Incorrect supplied qty error ([#23619](https://github.com/frappe/erpnext/pull/23619))
|
||||||
|
- Download Required Materials not working for production plan ([#23403](https://github.com/frappe/erpnext/pull/23403))
|
||||||
|
- Make account number length configurable ([#23496](https://github.com/frappe/erpnext/pull/23496))
|
||||||
|
- Include item_code in items result to allow adding product info in custom templates ([#23440](https://github.com/frappe/erpnext/pull/23440))
|
||||||
|
- Mode of payment getting overwritten by default mode of payment for returns ([#23599](https://github.com/frappe/erpnext/pull/23599))
|
||||||
|
- Show total row in print format of financial statement ([#23565](https://github.com/frappe/erpnext/pull/23565))
|
||||||
|
- Performance issue while adding template item in the cart ([#23508](https://github.com/frappe/erpnext/pull/23508))
|
||||||
|
- Display item name instead of item code in offline POS ([#23451](https://github.com/frappe/erpnext/pull/23451))
|
||||||
|
- Set stock UOM in item while creating material request from stock entry ([#23430](https://github.com/frappe/erpnext/pull/23430))
|
||||||
|
- Book loss amount in the COGS instead of stock received but not billed ([#23412](https://github.com/frappe/erpnext/pull/23412))
|
||||||
|
- Depreciation start date ux fixes ([#23340](https://github.com/frappe/erpnext/pull/23340))
|
||||||
|
- Payment Schedule not fetching ([#23477](https://github.com/frappe/erpnext/pull/23477))
|
||||||
|
- Taxable value in GSTR 3B report ([#23552](https://github.com/frappe/erpnext/pull/23552))
|
||||||
|
- Negative stock error while submitting stock reco for batch item ([#23564](https://github.com/frappe/erpnext/pull/23564))
|
||||||
|
- Check only Read and Write Permission in Update Items ([#23458](https://github.com/frappe/erpnext/pull/23458))
|
||||||
|
- Handle Blank from/to range in Numeric Item Attribute ([#23484](https://github.com/frappe/erpnext/pull/23484))
|
||||||
|
- Added new filters in the Batch-wise balance history report ([#23522](https://github.com/frappe/erpnext/pull/23522))
|
||||||
|
- Added filter show in website for filtering product ([#23638](https://github.com/frappe/erpnext/pull/23638))
|
||||||
|
- Enabled no copy property for Supplier Invoice Date to avoid due date validation ([#23367](https://github.com/frappe/erpnext/pull/23367))
|
||||||
|
- Showing a negative balance in expired leaves ([#23426](https://github.com/frappe/erpnext/pull/23426))
|
||||||
|
- Validate if removed item attributes exist in variants ([#23591](https://github.com/frappe/erpnext/pull/23591))
|
||||||
|
- Longer timeout for company replace abbreviation ([#23442](https://github.com/frappe/erpnext/pull/23442))
|
||||||
|
- Online pos print not working ([#23377](https://github.com/frappe/erpnext/pull/23377))
|
||||||
|
- Last purchase rate in item prices report ([#23507](https://github.com/frappe/erpnext/pull/23507))
|
||||||
|
- Do not consider opening entries for TDS calculation ([#23598](https://github.com/frappe/erpnext/pull/23598))
|
||||||
|
- Project value is missing from procurement-tracker ([#23551](https://github.com/frappe/erpnext/pull/23551))
|
||||||
@@ -20,7 +20,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_t
|
|||||||
from erpnext.exceptions import InvalidCurrency
|
from erpnext.exceptions import InvalidCurrency
|
||||||
from six import text_type
|
from six import text_type
|
||||||
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.stock.get_item_details import get_item_warehouse
|
from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
|
||||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||||
|
|
||||||
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
|
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
|
||||||
@@ -1127,6 +1127,43 @@ def get_supplier_block_status(party_name):
|
|||||||
}
|
}
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
def set_child_tax_template_and_map(item, child_item, parent_doc):
|
||||||
|
args = {
|
||||||
|
'item_code': item.item_code,
|
||||||
|
'posting_date': parent_doc.transaction_date,
|
||||||
|
'tax_category': parent_doc.get('tax_category'),
|
||||||
|
'company': parent_doc.get('company')
|
||||||
|
}
|
||||||
|
|
||||||
|
child_item.item_tax_template = _get_item_tax_template(args, item.taxes)
|
||||||
|
if child_item.get("item_tax_template"):
|
||||||
|
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
|
||||||
|
|
||||||
|
def add_taxes_from_tax_template(child_item, parent_doc):
|
||||||
|
add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template")
|
||||||
|
|
||||||
|
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
|
||||||
|
tax_map = json.loads(child_item.get("item_tax_rate"))
|
||||||
|
for tax_type in tax_map:
|
||||||
|
tax_rate = flt(tax_map[tax_type])
|
||||||
|
taxes = parent_doc.get('taxes') or []
|
||||||
|
# add new row for tax head only if missing
|
||||||
|
found = any(tax.account_head == tax_type for tax in taxes)
|
||||||
|
if not found:
|
||||||
|
tax_row = parent_doc.append("taxes", {})
|
||||||
|
tax_row.update({
|
||||||
|
"description" : str(tax_type).split(' - ')[0],
|
||||||
|
"charge_type" : "On Net Total",
|
||||||
|
"account_head" : tax_type,
|
||||||
|
"rate" : tax_rate
|
||||||
|
})
|
||||||
|
if parent_doc.doctype == "Purchase Order":
|
||||||
|
tax_row.update({
|
||||||
|
"category" : "Total",
|
||||||
|
"add_deduct_tax" : "Add"
|
||||||
|
})
|
||||||
|
tax_row.db_insert()
|
||||||
|
|
||||||
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
|
def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
|
||||||
"""
|
"""
|
||||||
Returns a Sales Order Item child item containing the default values
|
Returns a Sales Order Item child item containing the default values
|
||||||
@@ -1140,6 +1177,8 @@ def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname,
|
|||||||
child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
|
child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
|
||||||
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
|
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or get_conversion_factor(item.item_code, item.stock_uom).get("conversion_factor") or 1.0
|
||||||
child_item.uom = item.stock_uom
|
child_item.uom = item.stock_uom
|
||||||
|
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||||
|
add_taxes_from_tax_template(child_item, p_doc)
|
||||||
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
|
||||||
if not child_item.warehouse:
|
if not child_item.warehouse:
|
||||||
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
|
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
|
||||||
@@ -1162,6 +1201,8 @@ def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docna
|
|||||||
child_item.uom = item.stock_uom
|
child_item.uom = item.stock_uom
|
||||||
child_item.base_rate = 1 # Initiallize value will update in parent validation
|
child_item.base_rate = 1 # Initiallize value will update in parent validation
|
||||||
child_item.base_amount = 1 # Initiallize value will update in parent validation
|
child_item.base_amount = 1 # Initiallize value will update in parent validation
|
||||||
|
set_child_tax_template_and_map(item, child_item, p_doc)
|
||||||
|
add_taxes_from_tax_template(child_item, p_doc)
|
||||||
return child_item
|
return child_item
|
||||||
|
|
||||||
def validate_and_delete_children(parent, data):
|
def validate_and_delete_children(parent, data):
|
||||||
@@ -1270,19 +1311,21 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
validate_quantity(child_item, d)
|
validate_quantity(child_item, d)
|
||||||
|
|
||||||
child_item.qty = flt(d.get("qty"))
|
child_item.qty = flt(d.get("qty"))
|
||||||
precision = child_item.precision("rate") or 2
|
rate_precision = child_item.precision("rate") or 2
|
||||||
|
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||||
|
qty_precision = child_item.precision("qty") or 2
|
||||||
|
|
||||||
if flt(child_item.billed_amt, precision) > flt(flt(d.get("rate")) * flt(d.get("qty")), precision):
|
if flt(child_item.billed_amt, rate_precision) > flt(flt(d.get("rate"), rate_precision) * flt(d.get("qty"), qty_precision), rate_precision):
|
||||||
frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.")
|
frappe.throw(_("Row #{0}: Cannot set Rate if amount is greater than billed amount for Item {1}.")
|
||||||
.format(child_item.idx, child_item.item_code))
|
.format(child_item.idx, child_item.item_code))
|
||||||
else:
|
else:
|
||||||
child_item.rate = flt(d.get("rate"))
|
child_item.rate = flt(d.get("rate"), rate_precision)
|
||||||
|
|
||||||
if d.get("conversion_factor"):
|
if d.get("conversion_factor"):
|
||||||
if child_item.stock_uom == child_item.uom:
|
if child_item.stock_uom == child_item.uom:
|
||||||
child_item.conversion_factor = 1
|
child_item.conversion_factor = 1
|
||||||
else:
|
else:
|
||||||
child_item.conversion_factor = flt(d.get('conversion_factor'))
|
child_item.conversion_factor = flt(d.get('conversion_factor'), conv_fac_precision)
|
||||||
|
|
||||||
if d.get("delivery_date") and parent_doctype == 'Sales Order':
|
if d.get("delivery_date") and parent_doctype == 'Sales Order':
|
||||||
child_item.delivery_date = d.get('delivery_date')
|
child_item.delivery_date = d.get('delivery_date')
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ from erpnext.stock.utils import get_incoming_rate
|
|||||||
from erpnext.stock.get_item_details import get_conversion_factor
|
from erpnext.stock.get_item_details import get_conversion_factor
|
||||||
from erpnext.stock.doctype.item.item import set_item_default
|
from erpnext.stock.doctype.item.item import set_item_default
|
||||||
from frappe.contacts.doctype.address.address import get_address_display
|
from frappe.contacts.doctype.address.address import get_address_display
|
||||||
|
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import StockController
|
from erpnext.controllers.stock_controller import StockController
|
||||||
|
|
||||||
@@ -53,10 +54,10 @@ class SellingController(StockController):
|
|||||||
super(SellingController, self).set_missing_values(for_validate)
|
super(SellingController, self).set_missing_values(for_validate)
|
||||||
|
|
||||||
# set contact and address details for customer, if they are not mentioned
|
# set contact and address details for customer, if they are not mentioned
|
||||||
self.set_missing_lead_customer_details()
|
self.set_missing_lead_customer_details(for_validate=for_validate)
|
||||||
self.set_price_list_and_item_details(for_validate=for_validate)
|
self.set_price_list_and_item_details(for_validate=for_validate)
|
||||||
|
|
||||||
def set_missing_lead_customer_details(self):
|
def set_missing_lead_customer_details(self, for_validate=False):
|
||||||
customer, lead = None, None
|
customer, lead = None, None
|
||||||
if getattr(self, "customer", None):
|
if getattr(self, "customer", None):
|
||||||
customer = self.customer
|
customer = self.customer
|
||||||
@@ -93,6 +94,11 @@ class SellingController(StockController):
|
|||||||
posting_date=self.get('transaction_date') or self.get('posting_date'),
|
posting_date=self.get('transaction_date') or self.get('posting_date'),
|
||||||
company=self.company))
|
company=self.company))
|
||||||
|
|
||||||
|
if self.get('taxes_and_charges') and not self.get('taxes') and not for_validate:
|
||||||
|
taxes = get_taxes_and_charges('Sales Taxes and Charges Template', self.taxes_and_charges)
|
||||||
|
for tax in taxes:
|
||||||
|
self.append('taxes', tax)
|
||||||
|
|
||||||
def set_price_list_and_item_details(self, for_validate=False):
|
def set_price_list_and_item_details(self, for_validate=False):
|
||||||
self.set_price_list_currency("Selling")
|
self.set_price_list_currency("Selling")
|
||||||
self.set_missing_item_details(for_validate=for_validate)
|
self.set_missing_item_details(for_validate=for_validate)
|
||||||
|
|||||||
@@ -629,22 +629,29 @@ class calculate_taxes_and_totals(object):
|
|||||||
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
|
self.doc.other_charges_calculation = get_itemised_tax_breakup_html(self.doc)
|
||||||
|
|
||||||
def update_paid_amount_for_return(self, total_amount_to_pay):
|
def update_paid_amount_for_return(self, total_amount_to_pay):
|
||||||
default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
|
existing_amount = 0
|
||||||
{'parent': self.doc.pos_profile, 'default': 1},
|
|
||||||
['mode_of_payment', 'type', 'account'], as_dict=1)
|
|
||||||
|
|
||||||
self.doc.payments = []
|
for payment in self.doc.payments:
|
||||||
|
existing_amount += payment.amount
|
||||||
|
|
||||||
if default_mode_of_payment:
|
# do not override user entered amount if equal to total_amount_to_pay
|
||||||
self.doc.append('payments', {
|
if existing_amount != total_amount_to_pay:
|
||||||
'mode_of_payment': default_mode_of_payment.mode_of_payment,
|
default_mode_of_payment = frappe.db.get_value('Sales Invoice Payment',
|
||||||
'type': default_mode_of_payment.type,
|
{'parent': self.doc.pos_profile, 'default': 1},
|
||||||
'account': default_mode_of_payment.account,
|
['mode_of_payment', 'type', 'account'], as_dict=1)
|
||||||
'amount': total_amount_to_pay
|
|
||||||
})
|
self.doc.payments = []
|
||||||
else:
|
|
||||||
self.doc.is_pos = 0
|
if default_mode_of_payment:
|
||||||
self.doc.pos_profile = ''
|
self.doc.append('payments', {
|
||||||
|
'mode_of_payment': default_mode_of_payment.mode_of_payment,
|
||||||
|
'type': default_mode_of_payment.type,
|
||||||
|
'account': default_mode_of_payment.account,
|
||||||
|
'amount': total_amount_to_pay
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
self.doc.is_pos = 0
|
||||||
|
self.doc.pos_profile = ''
|
||||||
|
|
||||||
self.calculate_paid_amount()
|
self.calculate_paid_amount()
|
||||||
|
|
||||||
|
|||||||
@@ -2,81 +2,90 @@
|
|||||||
# 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
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
import plaid
|
||||||
from frappe import _
|
import requests
|
||||||
from frappe.utils.password import get_decrypted_password
|
from plaid.errors import APIError, ItemError, InvalidRequestError
|
||||||
from plaid import Client
|
|
||||||
from plaid.errors import APIError, ItemError
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import requests
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
class PlaidConnector():
|
class PlaidConnector():
|
||||||
def __init__(self, access_token=None):
|
def __init__(self, access_token=None):
|
||||||
|
|
||||||
plaid_settings = frappe.get_single("Plaid Settings")
|
|
||||||
|
|
||||||
self.config = {
|
|
||||||
"plaid_client_id": plaid_settings.plaid_client_id,
|
|
||||||
"plaid_secret": get_decrypted_password("Plaid Settings", "Plaid Settings", 'plaid_secret'),
|
|
||||||
"plaid_public_key": plaid_settings.plaid_public_key,
|
|
||||||
"plaid_env": plaid_settings.plaid_env
|
|
||||||
}
|
|
||||||
|
|
||||||
self.client = Client(client_id=self.config.get("plaid_client_id"),
|
|
||||||
secret=self.config.get("plaid_secret"),
|
|
||||||
public_key=self.config.get("plaid_public_key"),
|
|
||||||
environment=self.config.get("plaid_env")
|
|
||||||
)
|
|
||||||
|
|
||||||
self.access_token = access_token
|
self.access_token = access_token
|
||||||
|
self.settings = frappe.get_single("Plaid Settings")
|
||||||
|
self.products = ["auth", "transactions"]
|
||||||
|
self.client_name = frappe.local.site
|
||||||
|
self.client = plaid.Client(
|
||||||
|
client_id=self.settings.plaid_client_id,
|
||||||
|
secret=self.settings.get_password("plaid_secret"),
|
||||||
|
environment=self.settings.plaid_env,
|
||||||
|
api_version="2019-05-29"
|
||||||
|
)
|
||||||
|
|
||||||
def get_access_token(self, public_token):
|
def get_access_token(self, public_token):
|
||||||
if public_token is None:
|
if public_token is None:
|
||||||
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
|
frappe.log_error(_("Public token is missing for this bank"), _("Plaid public token error"))
|
||||||
|
|
||||||
response = self.client.Item.public_token.exchange(public_token)
|
response = self.client.Item.public_token.exchange(public_token)
|
||||||
access_token = response['access_token']
|
access_token = response["access_token"]
|
||||||
|
|
||||||
return access_token
|
return access_token
|
||||||
|
|
||||||
|
def get_link_token(self):
|
||||||
|
token_request = {
|
||||||
|
"client_name": self.client_name,
|
||||||
|
"client_id": self.settings.plaid_client_id,
|
||||||
|
"secret": self.settings.plaid_secret,
|
||||||
|
"products": self.products,
|
||||||
|
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
|
||||||
|
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
|
||||||
|
"country_codes": ["US", "CA", "FR", "IE", "NL", "ES", "GB"],
|
||||||
|
"user": {
|
||||||
|
"client_user_id": frappe.generate_hash(frappe.session.user, length=32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = self.client.LinkToken.create(token_request)
|
||||||
|
except InvalidRequestError:
|
||||||
|
frappe.log_error(frappe.get_traceback(), _("Plaid invalid request error"))
|
||||||
|
frappe.msgprint(_("Please check your Plaid client ID and secret values"))
|
||||||
|
except APIError as e:
|
||||||
|
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
|
||||||
|
frappe.throw(_(str(e)), title=_("Authentication Failed"))
|
||||||
|
else:
|
||||||
|
return response["link_token"]
|
||||||
|
|
||||||
def auth(self):
|
def auth(self):
|
||||||
try:
|
try:
|
||||||
self.client.Auth.get(self.access_token)
|
self.client.Auth.get(self.access_token)
|
||||||
print("Authentication successful.....")
|
|
||||||
except ItemError as e:
|
except ItemError as e:
|
||||||
if e.code == 'ITEM_LOGIN_REQUIRED':
|
if e.code == "ITEM_LOGIN_REQUIRED":
|
||||||
pass
|
|
||||||
else:
|
|
||||||
pass
|
pass
|
||||||
except APIError as e:
|
except APIError as e:
|
||||||
if e.code == 'PLANNED_MAINTENANCE':
|
if e.code == "PLANNED_MAINTENANCE":
|
||||||
pass
|
|
||||||
else:
|
|
||||||
pass
|
pass
|
||||||
except requests.Timeout:
|
except requests.Timeout:
|
||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(e)
|
|
||||||
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
|
frappe.log_error(frappe.get_traceback(), _("Plaid authentication error"))
|
||||||
frappe.msgprint({"title": _("Authentication Failed"), "message":e, "raise_exception":1, "indicator":'red'})
|
frappe.throw(_(str(e)), title=_("Authentication Failed"))
|
||||||
|
|
||||||
def get_transactions(self, start_date, end_date, account_id=None):
|
def get_transactions(self, start_date, end_date, account_id=None):
|
||||||
|
self.auth()
|
||||||
|
kwargs = dict(
|
||||||
|
access_token=self.access_token,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date
|
||||||
|
)
|
||||||
|
if account_id:
|
||||||
|
kwargs.update(dict(account_ids=[account_id]))
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.auth()
|
response = self.client.Transactions.get(**kwargs)
|
||||||
if account_id:
|
transactions = response["transactions"]
|
||||||
account_ids = [account_id]
|
while len(transactions) < response["total_transactions"]:
|
||||||
|
|
||||||
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, account_ids=account_ids)
|
|
||||||
|
|
||||||
else:
|
|
||||||
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date)
|
|
||||||
|
|
||||||
transactions = response['transactions']
|
|
||||||
|
|
||||||
while len(transactions) < response['total_transactions']:
|
|
||||||
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
|
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
|
||||||
transactions.extend(response['transactions'])
|
transactions.extend(response["transactions"])
|
||||||
return transactions
|
return transactions
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
|
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
frappe.provide("erpnext.integrations");
|
frappe.provide("erpnext.integrations");
|
||||||
|
|
||||||
frappe.ui.form.on('Plaid Settings', {
|
frappe.ui.form.on('Plaid Settings', {
|
||||||
enabled: function(frm) {
|
enabled: function (frm) {
|
||||||
frm.toggle_reqd('plaid_client_id', frm.doc.enabled);
|
frm.toggle_reqd('plaid_client_id', frm.doc.enabled);
|
||||||
frm.toggle_reqd('plaid_secret', frm.doc.enabled);
|
frm.toggle_reqd('plaid_secret', frm.doc.enabled);
|
||||||
frm.toggle_reqd('plaid_public_key', frm.doc.enabled);
|
|
||||||
frm.toggle_reqd('plaid_env', frm.doc.enabled);
|
frm.toggle_reqd('plaid_env', frm.doc.enabled);
|
||||||
},
|
},
|
||||||
refresh: function(frm) {
|
|
||||||
if(frm.doc.enabled) {
|
refresh: function (frm) {
|
||||||
|
if (frm.doc.enabled) {
|
||||||
frm.add_custom_button('Link a new bank account', () => {
|
frm.add_custom_button('Link a new bank account', () => {
|
||||||
new erpnext.integrations.plaidLink(frm);
|
new erpnext.integrations.plaidLink(frm);
|
||||||
});
|
});
|
||||||
@@ -22,17 +22,16 @@ frappe.ui.form.on('Plaid Settings', {
|
|||||||
erpnext.integrations.plaidLink = class plaidLink {
|
erpnext.integrations.plaidLink = class plaidLink {
|
||||||
constructor(parent) {
|
constructor(parent) {
|
||||||
this.frm = parent;
|
this.frm = parent;
|
||||||
this.product = ["transactions", "auth"];
|
|
||||||
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
|
this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
|
||||||
this.init_config();
|
this.init_config();
|
||||||
}
|
}
|
||||||
|
|
||||||
init_config() {
|
async init_config() {
|
||||||
const me = this;
|
this.product = ["auth", "transactions"];
|
||||||
me.plaid_env = me.frm.doc.plaid_env;
|
this.plaid_env = this.frm.doc.plaid_env;
|
||||||
me.plaid_public_key = me.frm.doc.plaid_public_key;
|
this.client_name = frappe.boot.sitename;
|
||||||
me.client_name = frappe.boot.sitename;
|
this.token = await this.frm.call("get_link_token").then(resp => resp.message);
|
||||||
me.init_plaid();
|
this.init_plaid();
|
||||||
}
|
}
|
||||||
|
|
||||||
init_plaid() {
|
init_plaid() {
|
||||||
@@ -69,17 +68,17 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onScriptLoaded(me) {
|
onScriptLoaded(me) {
|
||||||
me.linkHandler = window.Plaid.create({
|
me.linkHandler = Plaid.create({
|
||||||
clientName: me.client_name,
|
clientName: me.client_name,
|
||||||
|
product: me.product,
|
||||||
env: me.plaid_env,
|
env: me.plaid_env,
|
||||||
key: me.plaid_public_key,
|
token: me.token,
|
||||||
onSuccess: me.plaid_success,
|
onSuccess: me.plaid_success
|
||||||
product: me.product
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
onScriptError(error) {
|
onScriptError(error) {
|
||||||
frappe.msgprint('There was an issue loading the link-initialize.js script');
|
frappe.msgprint("There was an issue connecting to Plaid's authentication server");
|
||||||
frappe.msgprint(error);
|
frappe.msgprint(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,21 +86,25 @@ erpnext.integrations.plaidLink = class plaidLink {
|
|||||||
const me = this;
|
const me = this;
|
||||||
|
|
||||||
frappe.prompt({
|
frappe.prompt({
|
||||||
fieldtype:"Link",
|
fieldtype: "Link",
|
||||||
options: "Company",
|
options: "Company",
|
||||||
label:__("Company"),
|
label: __("Company"),
|
||||||
fieldname:"company",
|
fieldname: "company",
|
||||||
reqd:1
|
reqd: 1
|
||||||
}, (data) => {
|
}, (data) => {
|
||||||
me.company = data.company;
|
me.company = data.company;
|
||||||
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {token: token, response: response})
|
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_institution', {
|
||||||
.then((result) => {
|
token: token,
|
||||||
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {response: response,
|
response: response
|
||||||
bank: result, company: me.company});
|
}).then((result) => {
|
||||||
})
|
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.add_bank_accounts', {
|
||||||
.then(() => {
|
response: response,
|
||||||
frappe.show_alert({message:__("Bank accounts added"), indicator:'green'});
|
bank: result,
|
||||||
|
company: me.company
|
||||||
});
|
});
|
||||||
|
}).then(() => {
|
||||||
|
frappe.show_alert({ message: __("Bank accounts added"), indicator: 'green' });
|
||||||
|
});
|
||||||
}, __("Select a company"), __("Continue"));
|
}, __("Select a company"), __("Continue"));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
@@ -12,7 +11,6 @@
|
|||||||
"plaid_client_id",
|
"plaid_client_id",
|
||||||
"plaid_secret",
|
"plaid_secret",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
"plaid_public_key",
|
|
||||||
"plaid_env"
|
"plaid_env"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@@ -41,12 +39,6 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Plaid Secret"
|
"label": "Plaid Secret"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"fieldname": "plaid_public_key",
|
|
||||||
"fieldtype": "Data",
|
|
||||||
"in_list_view": 1,
|
|
||||||
"label": "Plaid Public Key"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "plaid_env",
|
"fieldname": "plaid_env",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
@@ -69,8 +61,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"modified": "2020-09-12 02:31:44.542385",
|
||||||
"modified": "2020-02-07 15:21:11.616231",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "ERPNext Integrations",
|
"module": "ERPNext Integrations",
|
||||||
"name": "Plaid Settings",
|
"name": "Plaid Settings",
|
||||||
|
|||||||
@@ -2,30 +2,36 @@
|
|||||||
# 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
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
|
||||||
import frappe
|
|
||||||
import json
|
import json
|
||||||
from frappe import _
|
|
||||||
from frappe.model.document import Document
|
import frappe
|
||||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||||
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
|
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_connector import PlaidConnector
|
||||||
from frappe.utils import getdate, formatdate, today, add_months
|
from frappe import _
|
||||||
from frappe.desk.doctype.tag.tag import add_tag
|
from frappe.desk.doctype.tag.tag import add_tag
|
||||||
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import add_months, formatdate, getdate, today
|
||||||
|
|
||||||
|
|
||||||
class PlaidSettings(Document):
|
class PlaidSettings(Document):
|
||||||
pass
|
@staticmethod
|
||||||
|
def get_link_token():
|
||||||
|
plaid = PlaidConnector()
|
||||||
|
return plaid.get_link_token()
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def plaid_configuration():
|
def get_plaid_configuration():
|
||||||
if frappe.db.get_single_value("Plaid Settings", "enabled"):
|
if frappe.db.get_single_value("Plaid Settings", "enabled"):
|
||||||
plaid_settings = frappe.get_single("Plaid Settings")
|
plaid_settings = frappe.get_single("Plaid Settings")
|
||||||
return {
|
return {
|
||||||
"plaid_public_key": plaid_settings.plaid_public_key,
|
|
||||||
"plaid_env": plaid_settings.plaid_env,
|
"plaid_env": plaid_settings.plaid_env,
|
||||||
|
"link_token": plaid_settings.get_link_token(),
|
||||||
"client_name": frappe.local.site
|
"client_name": frappe.local.site
|
||||||
}
|
}
|
||||||
else:
|
|
||||||
return "disabled"
|
return "disabled"
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_institution(token, response):
|
def add_institution(token, response):
|
||||||
@@ -33,6 +39,7 @@ def add_institution(token, response):
|
|||||||
|
|
||||||
plaid = PlaidConnector()
|
plaid = PlaidConnector()
|
||||||
access_token = plaid.get_access_token(token)
|
access_token = plaid.get_access_token(token)
|
||||||
|
bank = None
|
||||||
|
|
||||||
if not frappe.db.exists("Bank", response["institution"]["name"]):
|
if not frappe.db.exists("Bank", response["institution"]["name"]):
|
||||||
try:
|
try:
|
||||||
@@ -44,7 +51,6 @@ def add_institution(token, response):
|
|||||||
bank.insert()
|
bank.insert()
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.throw(frappe.get_traceback())
|
frappe.throw(frappe.get_traceback())
|
||||||
|
|
||||||
else:
|
else:
|
||||||
bank = frappe.get_doc("Bank", response["institution"]["name"])
|
bank = frappe.get_doc("Bank", response["institution"]["name"])
|
||||||
bank.plaid_access_token = access_token
|
bank.plaid_access_token = access_token
|
||||||
@@ -52,6 +58,7 @@ def add_institution(token, response):
|
|||||||
|
|
||||||
return bank
|
return bank
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def add_bank_accounts(response, bank, company):
|
def add_bank_accounts(response, bank, company):
|
||||||
try:
|
try:
|
||||||
@@ -92,9 +99,8 @@ def add_bank_accounts(response, bank, company):
|
|||||||
new_account.insert()
|
new_account.insert()
|
||||||
|
|
||||||
result.append(new_account.name)
|
result.append(new_account.name)
|
||||||
|
|
||||||
except frappe.UniqueValidationError:
|
except frappe.UniqueValidationError:
|
||||||
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(new_account.account_name))
|
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.throw(frappe.get_traceback())
|
frappe.throw(frappe.get_traceback())
|
||||||
|
|
||||||
@@ -103,6 +109,7 @@ def add_bank_accounts(response, bank, company):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def add_account_type(account_type):
|
def add_account_type(account_type):
|
||||||
try:
|
try:
|
||||||
frappe.get_doc({
|
frappe.get_doc({
|
||||||
@@ -122,10 +129,11 @@ def add_account_subtype(account_subtype):
|
|||||||
except Exception:
|
except Exception:
|
||||||
frappe.throw(frappe.get_traceback())
|
frappe.throw(frappe.get_traceback())
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def sync_transactions(bank, bank_account):
|
def sync_transactions(bank, bank_account):
|
||||||
''' Sync transactions based on the last integration date as the start date, after sync is completed
|
"""Sync transactions based on the last integration date as the start date, after sync is completed
|
||||||
add the transaction date of the oldest transaction as the last integration date '''
|
add the transaction date of the oldest transaction as the last integration date."""
|
||||||
|
|
||||||
last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
|
last_transaction_date = frappe.db.get_value("Bank Account", bank_account, "last_integration_date")
|
||||||
if last_transaction_date:
|
if last_transaction_date:
|
||||||
@@ -148,10 +156,10 @@ def sync_transactions(bank, bank_account):
|
|||||||
len(result), bank_account, start_date, end_date))
|
len(result), bank_account, start_date, end_date))
|
||||||
|
|
||||||
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date)
|
frappe.db.set_value("Bank Account", bank_account, "last_integration_date", last_transaction_date)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
|
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
|
||||||
|
|
||||||
|
|
||||||
def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
||||||
access_token = None
|
access_token = None
|
||||||
|
|
||||||
@@ -169,6 +177,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
|
|||||||
|
|
||||||
return transactions
|
return transactions
|
||||||
|
|
||||||
|
|
||||||
def new_bank_transaction(transaction):
|
def new_bank_transaction(transaction):
|
||||||
result = []
|
result = []
|
||||||
|
|
||||||
@@ -183,8 +192,8 @@ def new_bank_transaction(transaction):
|
|||||||
|
|
||||||
status = "Pending" if transaction["pending"] == "True" else "Settled"
|
status = "Pending" if transaction["pending"] == "True" else "Settled"
|
||||||
|
|
||||||
|
tags = []
|
||||||
try:
|
try:
|
||||||
tags = []
|
|
||||||
tags += transaction["category"]
|
tags += transaction["category"]
|
||||||
tags += ["Plaid Cat. {}".format(transaction["category_id"])]
|
tags += ["Plaid Cat. {}".format(transaction["category_id"])]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
@@ -217,6 +226,7 @@ def new_bank_transaction(transaction):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def automatic_synchronization():
|
def automatic_synchronization():
|
||||||
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
|
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
|
||||||
|
|
||||||
@@ -224,4 +234,8 @@ def automatic_synchronization():
|
|||||||
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
|
plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
|
||||||
|
|
||||||
for plaid_account in plaid_accounts:
|
for plaid_account in plaid_accounts:
|
||||||
frappe.enqueue("erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions", bank=plaid_account.bank, bank_account=plaid_account.name)
|
frappe.enqueue(
|
||||||
|
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
|
||||||
|
bank=plaid_account.bank,
|
||||||
|
bank_account=plaid_account.name
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
from __future__ import unicode_literals
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
import frappe
|
|
||||||
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import plaid_configuration, add_account_type, add_account_subtype, new_bank_transaction, add_bank_accounts
|
|
||||||
import json
|
import json
|
||||||
from frappe.utils.response import json_handler
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
from erpnext.accounts.doctype.journal_entry.journal_entry import get_default_bank_cash_account
|
||||||
|
from erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings import (
|
||||||
|
add_account_subtype, add_account_type, add_bank_accounts,
|
||||||
|
new_bank_transaction, get_plaid_configuration)
|
||||||
|
from frappe.utils.response import json_handler
|
||||||
|
|
||||||
|
|
||||||
class TestPlaidSettings(unittest.TestCase):
|
class TestPlaidSettings(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@@ -31,7 +34,7 @@ class TestPlaidSettings(unittest.TestCase):
|
|||||||
|
|
||||||
def test_plaid_disabled(self):
|
def test_plaid_disabled(self):
|
||||||
frappe.db.set_value("Plaid Settings", None, "enabled", 0)
|
frappe.db.set_value("Plaid Settings", None, "enabled", 0)
|
||||||
self.assertTrue(plaid_configuration() == "disabled")
|
self.assertTrue(get_plaid_configuration() == "disabled")
|
||||||
|
|
||||||
def test_add_account_type(self):
|
def test_add_account_type(self):
|
||||||
add_account_type("brokerage")
|
add_account_type("brokerage")
|
||||||
@@ -64,7 +67,7 @@ class TestPlaidSettings(unittest.TestCase):
|
|||||||
'mask': '0000',
|
'mask': '0000',
|
||||||
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
|
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
|
||||||
'name': 'Plaid Checking'
|
'name': 'Plaid Checking'
|
||||||
}],
|
}],
|
||||||
'institution': {
|
'institution': {
|
||||||
'institution_id': 'ins_6',
|
'institution_id': 'ins_6',
|
||||||
'name': 'Citi'
|
'name': 'Citi'
|
||||||
@@ -100,7 +103,7 @@ class TestPlaidSettings(unittest.TestCase):
|
|||||||
'mask': '0000',
|
'mask': '0000',
|
||||||
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
|
'id': '6GbM6RRQgdfy3lAqGz4JUnpmR948WZFg8DjQK',
|
||||||
'name': 'Plaid Checking'
|
'name': 'Plaid Checking'
|
||||||
}],
|
}],
|
||||||
'institution': {
|
'institution': {
|
||||||
'institution_id': 'ins_6',
|
'institution_id': 'ins_6',
|
||||||
'name': 'Citi'
|
'name': 'Citi'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
# 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
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ class HolidayList(Document):
|
|||||||
|
|
||||||
|
|
||||||
def validate_days(self):
|
def validate_days(self):
|
||||||
if self.from_date > self.to_date:
|
if getdate(self.from_date) > getdate(self.to_date):
|
||||||
throw(_("To Date cannot be before From Date"))
|
throw(_("To Date cannot be before From Date"))
|
||||||
|
|
||||||
for day in self.get("holidays"):
|
for day in self.get("holidays"):
|
||||||
|
|||||||
@@ -244,9 +244,10 @@
|
|||||||
],
|
],
|
||||||
"icon": "fa fa-calendar",
|
"icon": "fa fa-calendar",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"max_attachments": 3,
|
"max_attachments": 3,
|
||||||
"modified": "2020-09-23 18:53:11.608446",
|
"modified": "2020-09-23 19:11:58.806837",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Leave Application",
|
"name": "Leave Application",
|
||||||
|
|||||||
@@ -251,6 +251,10 @@ def make_bom(**args):
|
|||||||
'rate': item_doc.valuation_rate or args.rate,
|
'rate': item_doc.valuation_rate or args.rate,
|
||||||
})
|
})
|
||||||
|
|
||||||
bom.insert(ignore_permissions=True)
|
if not args.do_not_save:
|
||||||
bom.submit()
|
bom.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
if not args.do_not_submit:
|
||||||
|
bom.submit()
|
||||||
|
|
||||||
return bom
|
return bom
|
||||||
|
|||||||
@@ -371,6 +371,49 @@ class TestWorkOrder(unittest.TestCase):
|
|||||||
ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
|
ste1 = frappe.get_doc(make_stock_entry(wo.name, "Manufacture", 1))
|
||||||
self.assertEqual(len(ste1.items), 3)
|
self.assertEqual(len(ste1.items), 3)
|
||||||
|
|
||||||
|
def test_operation_time_with_batch_size(self):
|
||||||
|
fg_item = "Test Batch Size Item For BOM"
|
||||||
|
rm1 = "Test Batch Size Item RM 1 For BOM"
|
||||||
|
|
||||||
|
for item in ["Test Batch Size Item For BOM", "Test Batch Size Item RM 1 For BOM"]:
|
||||||
|
make_item(item, {
|
||||||
|
"include_item_in_manufacturing": 1,
|
||||||
|
"is_stock_item": 1
|
||||||
|
})
|
||||||
|
|
||||||
|
bom_name = frappe.db.get_value("BOM",
|
||||||
|
{"item": fg_item, "is_active": 1, "with_operations": 1}, "name")
|
||||||
|
|
||||||
|
if not bom_name:
|
||||||
|
bom = make_bom(item=fg_item, rate=1000, raw_materials = [rm1], do_not_save=True)
|
||||||
|
bom.with_operations = 1
|
||||||
|
bom.append("operations", {
|
||||||
|
"operation": "_Test Operation 1",
|
||||||
|
"workstation": "_Test Workstation 1",
|
||||||
|
"description": "Test Data",
|
||||||
|
"operating_cost": 100,
|
||||||
|
"time_in_mins": 40,
|
||||||
|
"batch_size": 5
|
||||||
|
})
|
||||||
|
|
||||||
|
bom.save()
|
||||||
|
bom.submit()
|
||||||
|
bom_name = bom.name
|
||||||
|
|
||||||
|
work_order = make_wo_order_test_record(item=fg_item,
|
||||||
|
planned_start_date=now(), qty=1, do_not_save=True)
|
||||||
|
|
||||||
|
work_order.set_work_order_operations()
|
||||||
|
work_order.save()
|
||||||
|
self.assertEqual(work_order.operations[0].time_in_mins, 8.0)
|
||||||
|
|
||||||
|
work_order1 = make_wo_order_test_record(item=fg_item,
|
||||||
|
planned_start_date=now(), qty=5, do_not_save=True)
|
||||||
|
|
||||||
|
work_order1.set_work_order_operations()
|
||||||
|
work_order1.save()
|
||||||
|
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
|
||||||
|
|
||||||
def get_scrap_item_details(bom_no):
|
def get_scrap_item_details(bom_no):
|
||||||
scrap_items = {}
|
scrap_items = {}
|
||||||
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
|
||||||
|
|||||||
@@ -611,7 +611,7 @@ erpnext.work_order = {
|
|||||||
description: __('Max: {0}', [max]),
|
description: __('Max: {0}', [max]),
|
||||||
default: max
|
default: max
|
||||||
}, data => {
|
}, data => {
|
||||||
max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
|
max += (frm.doc.qty * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100;
|
||||||
|
|
||||||
if (data.qty > max) {
|
if (data.qty > max) {
|
||||||
frappe.msgprint(__('Quantity must not be more than {0}', [max]));
|
frappe.msgprint(__('Quantity must not be more than {0}', [max]));
|
||||||
|
|||||||
@@ -364,7 +364,7 @@ class WorkOrder(Document):
|
|||||||
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
|
bom_qty = frappe.db.get_value("BOM", self.bom_no, "quantity")
|
||||||
|
|
||||||
for d in self.get("operations"):
|
for d in self.get("operations"):
|
||||||
d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * math.ceil(flt(self.qty) / flt(d.batch_size))
|
d.time_in_mins = flt(d.time_in_mins) / flt(bom_qty) * (flt(self.qty) / flt(d.batch_size))
|
||||||
|
|
||||||
self.calculate_operating_cost()
|
self.calculate_operating_cost()
|
||||||
|
|
||||||
|
|||||||
@@ -4,17 +4,16 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings")
|
frappe.reload_doc("erpnext_integrations", "doctype", "plaid_settings")
|
||||||
plaid_settings = frappe.get_single("Plaid Settings")
|
plaid_settings = frappe.get_single("Plaid Settings")
|
||||||
if plaid_settings.enabled:
|
if plaid_settings.enabled:
|
||||||
if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env \
|
if not (frappe.conf.plaid_client_id and frappe.conf.plaid_env and frappe.conf.plaid_secret):
|
||||||
and frappe.conf.plaid_public_key and frappe.conf.plaid_secret):
|
|
||||||
plaid_settings.enabled = 0
|
plaid_settings.enabled = 0
|
||||||
else:
|
else:
|
||||||
plaid_settings.update({
|
plaid_settings.update({
|
||||||
"plaid_client_id": frappe.conf.plaid_client_id,
|
"plaid_client_id": frappe.conf.plaid_client_id,
|
||||||
"plaid_public_key": frappe.conf.plaid_public_key,
|
|
||||||
"plaid_env": frappe.conf.plaid_env,
|
"plaid_env": frappe.conf.plaid_env,
|
||||||
"plaid_secret": frappe.conf.plaid_secret
|
"plaid_secret": frappe.conf.plaid_secret
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -14,13 +14,15 @@ def get_field_filter_data():
|
|||||||
for f in fields:
|
for f in fields:
|
||||||
doctype = f.get_link_doctype()
|
doctype = f.get_link_doctype()
|
||||||
|
|
||||||
# apply enable/disable filter
|
# apply enable/disable/show_in_website filter
|
||||||
meta = frappe.get_meta(doctype)
|
meta = frappe.get_meta(doctype)
|
||||||
filters = {}
|
filters = {}
|
||||||
if meta.has_field('enabled'):
|
if meta.has_field('enabled'):
|
||||||
filters['enabled'] = 1
|
filters['enabled'] = 1
|
||||||
if meta.has_field('disabled'):
|
if meta.has_field('disabled'):
|
||||||
filters['disabled'] = 0
|
filters['disabled'] = 0
|
||||||
|
if meta.has_field('show_in_website'):
|
||||||
|
filters['show_in_website'] = 1
|
||||||
|
|
||||||
values = [d.name for d in frappe.get_all(doctype, filters)]
|
values = [d.name for d in frappe.get_all(doctype, filters)]
|
||||||
filter_data.append([f, values])
|
filter_data.append([f, values])
|
||||||
@@ -378,7 +380,7 @@ def get_items(filters=None, search=None):
|
|||||||
|
|
||||||
results = frappe.db.sql('''
|
results = frappe.db.sql('''
|
||||||
SELECT
|
SELECT
|
||||||
`tabItem`.`name`, `tabItem`.`item_name`,
|
`tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
|
||||||
`tabItem`.`website_image`, `tabItem`.`image`,
|
`tabItem`.`website_image`, `tabItem`.`image`,
|
||||||
`tabItem`.`web_long_description`, `tabItem`.`description`,
|
`tabItem`.`web_long_description`, `tabItem`.`description`,
|
||||||
`tabItem`.`route`
|
`tabItem`.`route`
|
||||||
|
|||||||
@@ -672,25 +672,33 @@ erpnext.taxes_and_totals = erpnext.payments.extend({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1},
|
let existing_amount = 0
|
||||||
['mode_of_payment', 'account', 'type'], (value) => {
|
$.each(this.frm.doc.payments || [], function(i, row) {
|
||||||
if (this.frm.is_dirty()) {
|
existing_amount += row.amount;
|
||||||
frappe.model.clear_table(this.frm.doc, 'payments');
|
})
|
||||||
if (value) {
|
|
||||||
let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments');
|
|
||||||
row.mode_of_payment = value.mode_of_payment;
|
|
||||||
row.type = value.type;
|
|
||||||
row.account = value.account;
|
|
||||||
row.default = 1;
|
|
||||||
row.amount = total_amount_to_pay;
|
|
||||||
} else {
|
|
||||||
this.frm.set_value('is_pos', 1);
|
|
||||||
}
|
|
||||||
this.frm.refresh_fields();
|
|
||||||
}
|
|
||||||
}, 'Sales Invoice');
|
|
||||||
|
|
||||||
this.calculate_paid_amount();
|
if (existing_amount != total_amount_to_pay) {
|
||||||
|
frappe.db.get_value('Sales Invoice Payment', {'parent': this.frm.doc.pos_profile, 'default': 1},
|
||||||
|
['mode_of_payment', 'account', 'type'], (value) => {
|
||||||
|
if (this.frm.is_dirty()) {
|
||||||
|
frappe.model.clear_table(this.frm.doc, 'payments');
|
||||||
|
if (value) {
|
||||||
|
let row = frappe.model.add_child(this.frm.doc, 'Sales Invoice Payment', 'payments');
|
||||||
|
row.mode_of_payment = value.mode_of_payment;
|
||||||
|
row.type = value.type;
|
||||||
|
row.account = value.account;
|
||||||
|
row.default = 1;
|
||||||
|
row.amount = total_amount_to_pay;
|
||||||
|
} else {
|
||||||
|
this.frm.set_value('is_pos', 1);
|
||||||
|
}
|
||||||
|
this.frm.refresh_fields();
|
||||||
|
this.calculate_paid_amount();
|
||||||
|
}
|
||||||
|
}, 'Sales Invoice');
|
||||||
|
} else {
|
||||||
|
this.calculate_paid_amount();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
set_default_payment: function(total_amount_to_pay, update_paid_amount) {
|
set_default_payment: function(total_amount_to_pay, update_paid_amount) {
|
||||||
|
|||||||
@@ -452,6 +452,9 @@ erpnext.utils.update_child_items = function(opts) {
|
|||||||
const frm = opts.frm;
|
const frm = opts.frm;
|
||||||
const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row;
|
const cannot_add_row = (typeof opts.cannot_add_row === 'undefined') ? true : opts.cannot_add_row;
|
||||||
const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname;
|
const child_docname = (typeof opts.cannot_add_row === 'undefined') ? "items" : opts.child_docname;
|
||||||
|
const child_meta = frappe.get_meta(`${frm.doc.doctype} Item`);
|
||||||
|
const get_precision = (fieldname) => child_meta.fields.find(f => f.fieldname == fieldname).precision;
|
||||||
|
|
||||||
this.data = [];
|
this.data = [];
|
||||||
const fields = [{
|
const fields = [{
|
||||||
fieldtype:'Data',
|
fieldtype:'Data',
|
||||||
@@ -472,14 +475,16 @@ erpnext.utils.update_child_items = function(opts) {
|
|||||||
default: 0,
|
default: 0,
|
||||||
read_only: 0,
|
read_only: 0,
|
||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
label: __('Qty')
|
label: __('Qty'),
|
||||||
|
precision: get_precision("qty")
|
||||||
}, {
|
}, {
|
||||||
fieldtype:'Currency',
|
fieldtype:'Currency',
|
||||||
fieldname:"rate",
|
fieldname:"rate",
|
||||||
default: 0,
|
default: 0,
|
||||||
read_only: 0,
|
read_only: 0,
|
||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
label: __('Rate')
|
label: __('Rate'),
|
||||||
|
precision: get_precision("rate")
|
||||||
}];
|
}];
|
||||||
|
|
||||||
if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) {
|
if (frm.doc.doctype == 'Sales Order' || frm.doc.doctype == 'Purchase Order' ) {
|
||||||
@@ -494,7 +499,8 @@ erpnext.utils.update_child_items = function(opts) {
|
|||||||
fieldtype: 'Float',
|
fieldtype: 'Float',
|
||||||
fieldname: "conversion_factor",
|
fieldname: "conversion_factor",
|
||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
label: __("Conversion Factor")
|
label: __("Conversion Factor"),
|
||||||
|
precision: get_precision('conversion_factor')
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,5 +10,13 @@ frappe.ui.form.on('Quality Procedure', {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query('parent_quality_procedure', function(){
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
is_group: 1
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -21,8 +21,7 @@
|
|||||||
"fieldname": "parent_quality_procedure",
|
"fieldname": "parent_quality_procedure",
|
||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"label": "Parent Procedure",
|
"label": "Parent Procedure",
|
||||||
"options": "Quality Procedure",
|
"options": "Quality Procedure"
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@@ -73,7 +72,7 @@
|
|||||||
],
|
],
|
||||||
"is_tree": 1,
|
"is_tree": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-06-17 17:25:03.434953",
|
"modified": "2020-10-12 16:14:11.167537",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Quality Management",
|
"module": "Quality Management",
|
||||||
"name": "Quality Procedure",
|
"name": "Quality Procedure",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils.nestedset import NestedSet
|
from frappe.utils.nestedset import NestedSet, rebuild_tree
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
|
||||||
class QualityProcedure(NestedSet):
|
class QualityProcedure(NestedSet):
|
||||||
@@ -42,6 +42,8 @@ class QualityProcedure(NestedSet):
|
|||||||
doc.save(ignore_permissions=True)
|
doc.save(ignore_permissions=True)
|
||||||
|
|
||||||
def set_parent(self):
|
def set_parent(self):
|
||||||
|
rebuild_tree('Quality Procedure', 'parent_quality_procedure')
|
||||||
|
|
||||||
for process in self.processes:
|
for process in self.processes:
|
||||||
# Set parent for only those children who don't have a parent
|
# Set parent for only those children who don't have a parent
|
||||||
parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure")
|
parent_quality_procedure = frappe.db.get_value("Quality Procedure", process.procedure, "parent_quality_procedure")
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"client",
|
"client",
|
||||||
|
"account_number_length",
|
||||||
"column_break_2",
|
"column_break_2",
|
||||||
"client_number",
|
"client_number",
|
||||||
"section_break_4",
|
"section_break_4",
|
||||||
@@ -57,9 +58,16 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "column_break_6",
|
"fieldname": "column_break_6",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "4",
|
||||||
|
"fieldname": "account_number_length",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"label": "Account Number Length",
|
||||||
|
"reqd": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2020-04-15 12:59:57.786506",
|
"modified": "2020-10-03 16:52:35.903867",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Regional",
|
"module": "Regional",
|
||||||
"name": "DATEV Settings",
|
"name": "DATEV Settings",
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ class GSTR3BReport(Document):
|
|||||||
|
|
||||||
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"])
|
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"])
|
||||||
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"])
|
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"])
|
||||||
self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y")
|
self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas", "Registered Regular"], reverse_charge="Y")
|
||||||
self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2)
|
self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2)
|
||||||
self.set_itc_details(itc_details)
|
self.set_itc_details(itc_details)
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ class GSTR3BReport(Document):
|
|||||||
if d["ty"] == 'ISRC':
|
if d["ty"] == 'ISRC':
|
||||||
reverse_charge = "Y"
|
reverse_charge = "Y"
|
||||||
itc_type = 'All Other ITC'
|
itc_type = 'All Other ITC'
|
||||||
gst_category = ['Unregistered', 'Overseas']
|
gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
|
||||||
else:
|
else:
|
||||||
reverse_charge = "N"
|
reverse_charge = "N"
|
||||||
|
|
||||||
@@ -253,7 +253,7 @@ class GSTR3BReport(Document):
|
|||||||
def get_total_taxable_value(self, doctype, reverse_charge):
|
def get_total_taxable_value(self, doctype, reverse_charge):
|
||||||
|
|
||||||
return frappe._dict(frappe.db.sql("""
|
return frappe._dict(frappe.db.sql("""
|
||||||
select gst_category, sum(net_total) as total
|
select gst_category, sum(base_net_total) as total
|
||||||
from `tab{doctype}`
|
from `tab{doctype}`
|
||||||
where docstatus = 1 and month(posting_date) = %s
|
where docstatus = 1 and month(posting_date) = %s
|
||||||
and year(posting_date) = %s and reverse_charge = %s
|
and year(posting_date) = %s and reverse_charge = %s
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ from csv import QUOTE_NONNUMERIC
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from erpnext.accounts.utils import get_fiscal_year
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
@@ -30,20 +31,33 @@ def execute(filters=None):
|
|||||||
|
|
||||||
def validate(filters):
|
def validate(filters):
|
||||||
"""Make sure all mandatory filters and settings are present."""
|
"""Make sure all mandatory filters and settings are present."""
|
||||||
if not filters.get('company'):
|
company = filters.get('company')
|
||||||
|
if not company:
|
||||||
frappe.throw(_('<b>Company</b> is a mandatory filter.'))
|
frappe.throw(_('<b>Company</b> is a mandatory filter.'))
|
||||||
|
|
||||||
if not filters.get('from_date'):
|
from_date = filters.get('from_date')
|
||||||
|
if not from_date:
|
||||||
frappe.throw(_('<b>From Date</b> is a mandatory filter.'))
|
frappe.throw(_('<b>From Date</b> is a mandatory filter.'))
|
||||||
|
|
||||||
if not filters.get('to_date'):
|
to_date = filters.get('to_date')
|
||||||
|
if not to_date:
|
||||||
frappe.throw(_('<b>To Date</b> is a mandatory filter.'))
|
frappe.throw(_('<b>To Date</b> is a mandatory filter.'))
|
||||||
|
|
||||||
|
validate_fiscal_year(from_date, to_date, company)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
frappe.get_doc('DATEV Settings', filters.get('company'))
|
frappe.get_doc('DATEV Settings', filters.get('company'))
|
||||||
except frappe.DoesNotExistError:
|
except frappe.DoesNotExistError:
|
||||||
frappe.throw(_('Please create <b>DATEV Settings</b> for Company <b>{}</b>.').format(filters.get('company')))
|
frappe.throw(_('Please create <b>DATEV Settings</b> for Company <b>{}</b>.').format(filters.get('company')))
|
||||||
|
|
||||||
|
|
||||||
|
def validate_fiscal_year(from_date, to_date, company):
|
||||||
|
from_fiscal_year = get_fiscal_year(date=from_date, company=company)
|
||||||
|
to_fiscal_year = get_fiscal_year(date=to_date, company=company)
|
||||||
|
if from_fiscal_year != to_fiscal_year:
|
||||||
|
frappe.throw(_('Dates {} and {} are not in the same fiscal year.').format(from_date, to_date))
|
||||||
|
|
||||||
|
|
||||||
def get_columns():
|
def get_columns():
|
||||||
"""Return the list of columns that will be shown in query report."""
|
"""Return the list of columns that will be shown in query report."""
|
||||||
columns = [
|
columns = [
|
||||||
@@ -231,9 +245,9 @@ def get_datev_csv(data, filters):
|
|||||||
# L = Tax client number (Mandantennummer)
|
# L = Tax client number (Mandantennummer)
|
||||||
frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "",
|
frappe.get_value("DATEV Settings", filters.get("company"), "client_number") or "",
|
||||||
# M = Start of the fiscal year (Wirtschaftsjahresbeginn)
|
# M = Start of the fiscal year (Wirtschaftsjahresbeginn)
|
||||||
frappe.utils.formatdate(frappe.defaults.get_user_default("year_start_date"), "yyyyMMdd"),
|
frappe.utils.formatdate(filters.get("fiscal_year_start"), "yyyyMMdd"),
|
||||||
# N = Length of account numbers (Sachkontenlänge)
|
# N = Length of account numbers (Sachkontenlänge)
|
||||||
"4",
|
str(filters.get('account_number_length', 4)),
|
||||||
# O = Transaction batch start date (YYYYMMDD)
|
# O = Transaction batch start date (YYYYMMDD)
|
||||||
frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"),
|
frappe.utils.formatdate(filters.get('from_date'), "yyyyMMdd"),
|
||||||
# P = Transaction batch end date (YYYYMMDD)
|
# P = Transaction batch end date (YYYYMMDD)
|
||||||
@@ -507,6 +521,12 @@ def download_datev_csv(filters=None):
|
|||||||
filters = json.loads(filters)
|
filters = json.loads(filters)
|
||||||
|
|
||||||
validate(filters)
|
validate(filters)
|
||||||
|
|
||||||
|
filters['account_number_length'] = frappe.get_value('DATEV Settings', filters.get('company'), 'account_number_length')
|
||||||
|
|
||||||
|
fiscal_year = get_fiscal_year(date=filters.get('from_date'), company=filters.get('company'))
|
||||||
|
filters['fiscal_year_start'] = fiscal_year[1]
|
||||||
|
|
||||||
data = get_gl_entries(filters, as_dict=1)
|
data = get_gl_entries(filters, as_dict=1)
|
||||||
|
|
||||||
frappe.response['result'] = get_datev_csv(data, filters)
|
frappe.response['result'] = get_datev_csv(data, filters)
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ class Customer(TransactionBase):
|
|||||||
address = frappe.get_doc('Address', address_name.get('name'))
|
address = frappe.get_doc('Address', address_name.get('name'))
|
||||||
if not address.has_link('Customer', self.name):
|
if not address.has_link('Customer', self.name):
|
||||||
address.append('links', dict(link_doctype='Customer', link_name=self.name))
|
address.append('links', dict(link_doctype='Customer', link_name=self.name))
|
||||||
address.save()
|
address.save(ignore_permissions=self.flags.ignore_permissions)
|
||||||
|
|
||||||
lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True)
|
lead = frappe.db.get_value("Lead", self.lead_name, ["organization_lead", "lead_name", "email_id", "phone", "mobile_no", "gender", "salutation"], as_dict=True)
|
||||||
|
|
||||||
@@ -147,7 +147,7 @@ class Customer(TransactionBase):
|
|||||||
contact = frappe.get_doc('Contact', contact_name.get('name'))
|
contact = frappe.get_doc('Contact', contact_name.get('name'))
|
||||||
if not contact.has_link('Customer', self.name):
|
if not contact.has_link('Customer', self.name):
|
||||||
contact.append('links', dict(link_doctype='Customer', link_name=self.name))
|
contact.append('links', dict(link_doctype='Customer', link_name=self.name))
|
||||||
contact.save()
|
contact.save(ignore_permissions=self.flags.ignore_permissions)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
lead.lead_name = lead.lead_name.lstrip().split(" ")
|
lead.lead_name = lead.lead_name.lstrip().split(" ")
|
||||||
|
|||||||
@@ -108,6 +108,10 @@ class TestQuotation(unittest.TestCase):
|
|||||||
sales_order.transaction_date = nowdate()
|
sales_order.transaction_date = nowdate()
|
||||||
sales_order.insert()
|
sales_order.insert()
|
||||||
|
|
||||||
|
# Remove any unknown taxes if applied
|
||||||
|
sales_order.set('taxes', [])
|
||||||
|
sales_order.save()
|
||||||
|
|
||||||
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
|
self.assertEqual(sales_order.payment_schedule[0].payment_amount, 8906.00)
|
||||||
self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date))
|
self.assertEqual(sales_order.payment_schedule[0].due_date, getdate(quotation.transaction_date))
|
||||||
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
|
self.assertEqual(sales_order.payment_schedule[1].payment_amount, 8906.00)
|
||||||
|
|||||||
@@ -169,7 +169,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// project
|
// project
|
||||||
if(flt(doc.per_delivered, 2) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) {
|
if(flt(doc.per_delivered, 2) < 100) {
|
||||||
this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create'));
|
this.frm.add_custom_button(__('Project'), () => this.make_project(), __('Create'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,8 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
self.assertEqual(len(si.get("items")), 1)
|
self.assertEqual(len(si.get("items")), 1)
|
||||||
|
|
||||||
si.insert()
|
si.insert()
|
||||||
|
si.set('taxes', [])
|
||||||
|
si.save()
|
||||||
|
|
||||||
self.assertEqual(si.payment_schedule[0].payment_amount, 500.0)
|
self.assertEqual(si.payment_schedule[0].payment_amount, 500.0)
|
||||||
self.assertEqual(si.payment_schedule[0].due_date, so.transaction_date)
|
self.assertEqual(si.payment_schedule[0].due_date, so.transaction_date)
|
||||||
@@ -401,6 +403,22 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}])
|
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 2, 'docname': so.items[0].name}])
|
||||||
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
|
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
|
||||||
|
|
||||||
|
def test_update_child_with_precision(self):
|
||||||
|
from frappe.model.meta import get_field_precision
|
||||||
|
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||||
|
|
||||||
|
precision = get_field_precision(frappe.get_meta("Sales Order Item").get_field("rate"))
|
||||||
|
|
||||||
|
make_property_setter("Sales Order Item", "rate", "precision", 7, "Currency")
|
||||||
|
so = make_sales_order(item_code= "_Test Item", qty=4, rate=200.34664)
|
||||||
|
|
||||||
|
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200.34669, 'qty' : 4, 'docname': so.items[0].name}])
|
||||||
|
update_child_qty_rate('Sales Order', trans_item, so.name)
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.items[0].rate, 200.34669)
|
||||||
|
make_property_setter("Sales Order Item", "rate", "precision", precision, "Currency")
|
||||||
|
|
||||||
def test_update_child_qty_rate_perm(self):
|
def test_update_child_qty_rate_perm(self):
|
||||||
so = make_sales_order(item_code= "_Test Item", qty=4)
|
so = make_sales_order(item_code= "_Test Item", qty=4)
|
||||||
|
|
||||||
@@ -425,9 +443,9 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
workflow = make_sales_order_workflow()
|
workflow = make_sales_order_workflow()
|
||||||
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
|
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
|
||||||
|
frappe.set_user("Administrator")
|
||||||
apply_workflow(so, 'Approve')
|
apply_workflow(so, 'Approve')
|
||||||
|
|
||||||
frappe.set_user("Administrator")
|
|
||||||
user = 'test@example.com'
|
user = 'test@example.com'
|
||||||
test_user = frappe.get_doc('User', user)
|
test_user = frappe.get_doc('User', user)
|
||||||
test_user.add_roles("Sales User", "Test Junior Approver")
|
test_user.add_roles("Sales User", "Test Junior Approver")
|
||||||
@@ -474,6 +492,95 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
so.reload()
|
so.reload()
|
||||||
self.assertEqual(so.packed_items[0].qty, 4)
|
self.assertEqual(so.packed_items[0].qty, 4)
|
||||||
|
|
||||||
|
def test_update_child_with_tax_template(self):
|
||||||
|
"""
|
||||||
|
Test Action: Create a SO with one item having its tax account head already in the SO.
|
||||||
|
Add the same item + new item with tax template via Update Items.
|
||||||
|
Expected result: First Item's tax row is updated. New tax row is added for second Item.
|
||||||
|
"""
|
||||||
|
if not frappe.db.exists("Item", "Test Item with Tax"):
|
||||||
|
make_item("Test Item with Tax", {
|
||||||
|
'is_stock_item': 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}):
|
||||||
|
frappe.get_doc({
|
||||||
|
'doctype': 'Item Tax Template',
|
||||||
|
'title': 'Test Update Items Template',
|
||||||
|
'company': '_Test Company',
|
||||||
|
'taxes': [
|
||||||
|
{
|
||||||
|
'tax_type': "_Test Account Service Tax - _TC",
|
||||||
|
'tax_rate': 10,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).insert()
|
||||||
|
|
||||||
|
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
|
||||||
|
|
||||||
|
new_item_with_tax.append("taxes", {
|
||||||
|
"item_tax_template": "Test Update Items Template",
|
||||||
|
"valid_from": nowdate()
|
||||||
|
})
|
||||||
|
new_item_with_tax.save()
|
||||||
|
|
||||||
|
tax_template = "_Test Account Excise Duty @ 10"
|
||||||
|
item = "_Test Item Home Desktop 100"
|
||||||
|
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
|
||||||
|
item_doc = frappe.get_doc("Item", item)
|
||||||
|
item_doc.append("taxes", {
|
||||||
|
"item_tax_template": tax_template,
|
||||||
|
"valid_from": nowdate()
|
||||||
|
})
|
||||||
|
item_doc.save()
|
||||||
|
else:
|
||||||
|
# update valid from
|
||||||
|
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE()
|
||||||
|
where parent = %(item)s and item_tax_template = %(tax)s""",
|
||||||
|
{"item": item, "tax": tax_template})
|
||||||
|
|
||||||
|
so = make_sales_order(item_code=item, qty=1, do_not_save=1)
|
||||||
|
|
||||||
|
so.append("taxes", {
|
||||||
|
"account_head": "_Test Account Excise Duty - _TC",
|
||||||
|
"charge_type": "On Net Total",
|
||||||
|
"cost_center": "_Test Cost Center - _TC",
|
||||||
|
"description": "Excise Duty",
|
||||||
|
"doctype": "Sales Taxes and Charges",
|
||||||
|
"rate": 10
|
||||||
|
})
|
||||||
|
so.insert()
|
||||||
|
so.submit()
|
||||||
|
|
||||||
|
self.assertEqual(so.taxes[0].tax_amount, 10)
|
||||||
|
self.assertEqual(so.taxes[0].total, 110)
|
||||||
|
|
||||||
|
old_stock_settings_value = frappe.db.get_single_value("Stock Settings", "default_warehouse")
|
||||||
|
frappe.db.set_value("Stock Settings", None, "default_warehouse", "_Test Warehouse - _TC")
|
||||||
|
|
||||||
|
items = json.dumps([
|
||||||
|
{'item_code' : item, 'rate' : 100, 'qty' : 1, 'docname': so.items[0].name},
|
||||||
|
{'item_code' : item, 'rate' : 200, 'qty' : 1}, # added item whose tax account head already exists in PO
|
||||||
|
{'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head is missing in PO
|
||||||
|
])
|
||||||
|
update_child_qty_rate('Sales Order', items, so.name)
|
||||||
|
|
||||||
|
so.reload()
|
||||||
|
self.assertEqual(so.taxes[0].tax_amount, 40)
|
||||||
|
self.assertEqual(so.taxes[0].total, 440)
|
||||||
|
self.assertEqual(so.taxes[1].account_head, "_Test Account Service Tax - _TC")
|
||||||
|
self.assertEqual(so.taxes[1].tax_amount, 40)
|
||||||
|
self.assertEqual(so.taxes[1].total, 480)
|
||||||
|
|
||||||
|
# teardown
|
||||||
|
frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL
|
||||||
|
where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template})
|
||||||
|
so.cancel()
|
||||||
|
so.delete()
|
||||||
|
new_item_with_tax.delete()
|
||||||
|
frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
|
||||||
|
frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value)
|
||||||
|
|
||||||
def test_warehouse_user(self):
|
def test_warehouse_user(self):
|
||||||
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
|
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
|
||||||
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
|
frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
|
||||||
@@ -1010,7 +1117,7 @@ def make_sales_order_workflow():
|
|||||||
"is_active": 1,
|
"is_active": 1,
|
||||||
"send_email_alert": 0,
|
"send_email_alert": 0,
|
||||||
})
|
})
|
||||||
workflow.append('states', dict( state = 'Pending', allow_edit = 'All' ))
|
workflow.append('states', dict( state = 'Pending', allow_edit = 'Administrator' ))
|
||||||
workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 ))
|
workflow.append('states', dict( state = 'Approved', allow_edit = 'Test Approver', doc_status = 1 ))
|
||||||
workflow.append('transitions', dict(
|
workflow.append('transitions', dict(
|
||||||
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1,
|
state = 'Pending', action = 'Approve', next_state = 'Approved', allowed = 'Test Junior Approver', allow_self_approval = 1,
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ class Company(NestedSet):
|
|||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def enqueue_replace_abbr(company, old, new):
|
def enqueue_replace_abbr(company, old, new):
|
||||||
kwargs = dict(company=company, old=old, new=new)
|
kwargs = dict(queue='long', company=company, old=old, new=new)
|
||||||
frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs)
|
frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from frappe.utils import cstr
|
from frappe.utils import cstr, cint
|
||||||
from frappe import msgprint, throw, _
|
from frappe import msgprint, throw, _
|
||||||
|
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
@@ -159,7 +159,7 @@ class NamingSeries(Document):
|
|||||||
prefix = self.parse_naming_series()
|
prefix = self.parse_naming_series()
|
||||||
self.insert_series(prefix)
|
self.insert_series(prefix)
|
||||||
frappe.db.sql("update `tabSeries` set current = %s where name = %s",
|
frappe.db.sql("update `tabSeries` set current = %s where name = %s",
|
||||||
(self.current_value, prefix))
|
(cint(self.current_value), prefix))
|
||||||
msgprint(_("Series Updated Successfully"))
|
msgprint(_("Series Updated Successfully"))
|
||||||
else:
|
else:
|
||||||
msgprint(_("Please select prefix first"))
|
msgprint(_("Please select prefix first"))
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ class Item(WebsiteGenerator):
|
|||||||
self.synced_with_hub = 0
|
self.synced_with_hub = 0
|
||||||
|
|
||||||
self.validate_has_variants()
|
self.validate_has_variants()
|
||||||
|
self.validate_attributes_in_variants()
|
||||||
self.validate_stock_exists_for_template_item()
|
self.validate_stock_exists_for_template_item()
|
||||||
self.validate_attributes()
|
self.validate_attributes()
|
||||||
self.validate_variant_attributes()
|
self.validate_variant_attributes()
|
||||||
@@ -806,6 +807,76 @@ class Item(WebsiteGenerator):
|
|||||||
if frappe.db.exists("Item", {"variant_of": self.name}):
|
if frappe.db.exists("Item", {"variant_of": self.name}):
|
||||||
frappe.throw(_("Item has variants."))
|
frappe.throw(_("Item has variants."))
|
||||||
|
|
||||||
|
def validate_attributes_in_variants(self):
|
||||||
|
if not self.has_variants or self.get("__islocal"):
|
||||||
|
return
|
||||||
|
|
||||||
|
old_doc = self.get_doc_before_save()
|
||||||
|
old_doc_attributes = set([attr.attribute for attr in old_doc.attributes])
|
||||||
|
own_attributes = [attr.attribute for attr in self.attributes]
|
||||||
|
|
||||||
|
# Check if old attributes were removed from the list
|
||||||
|
# Is old_attrs is a subset of new ones
|
||||||
|
# that means we need not check any changes
|
||||||
|
if old_doc_attributes.issubset(set(own_attributes)):
|
||||||
|
return
|
||||||
|
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
|
# get all item variants
|
||||||
|
items = [item["name"] for item in frappe.get_all("Item", {"variant_of": self.name})]
|
||||||
|
|
||||||
|
# get all deleted attributes
|
||||||
|
deleted_attribute = list(old_doc_attributes.difference(set(own_attributes)))
|
||||||
|
|
||||||
|
# fetch all attributes of these items
|
||||||
|
item_attributes = frappe.get_all(
|
||||||
|
"Item Variant Attribute",
|
||||||
|
filters={
|
||||||
|
"parent": ["in", items],
|
||||||
|
"attribute": ["in", deleted_attribute]
|
||||||
|
},
|
||||||
|
fields=["attribute", "parent"]
|
||||||
|
)
|
||||||
|
not_included = defaultdict(list)
|
||||||
|
|
||||||
|
for attr in item_attributes:
|
||||||
|
if attr["attribute"] not in own_attributes:
|
||||||
|
not_included[attr["parent"]].append(attr["attribute"])
|
||||||
|
|
||||||
|
if not len(not_included):
|
||||||
|
return
|
||||||
|
|
||||||
|
def body(docnames):
|
||||||
|
docnames.sort()
|
||||||
|
return "<br>".join(docnames)
|
||||||
|
|
||||||
|
def table_row(title, body):
|
||||||
|
return """<tr>
|
||||||
|
<td>{0}</td>
|
||||||
|
<td>{1}</td>
|
||||||
|
</tr>""".format(title, body)
|
||||||
|
|
||||||
|
rows = ''
|
||||||
|
for docname, attr_list in not_included.items():
|
||||||
|
link = "<a href='#Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname)))
|
||||||
|
rows += table_row(link, body(attr_list))
|
||||||
|
|
||||||
|
error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.')
|
||||||
|
|
||||||
|
message = """
|
||||||
|
<div>{0}</div><br>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<td>{1}</td>
|
||||||
|
<td>{2}</td>
|
||||||
|
</thead>
|
||||||
|
{3}
|
||||||
|
</table>
|
||||||
|
""".format(error_description, _('Variant Items'), _('Attributes'), rows)
|
||||||
|
|
||||||
|
frappe.throw(message, title=_("Variant Attribute Error"), is_minimizable=True)
|
||||||
|
|
||||||
def validate_stock_exists_for_template_item(self):
|
def validate_stock_exists_for_template_item(self):
|
||||||
if self.stock_ledger_created() and self._doc_before_save:
|
if self.stock_ledger_created() and self._doc_before_save:
|
||||||
if (cint(self._doc_before_save.has_variants) != cint(self.has_variants)
|
if (cint(self._doc_before_save.has_variants) != cint(self.has_variants)
|
||||||
@@ -1069,8 +1140,7 @@ def invalidate_item_variants_cache_for_website(doc):
|
|||||||
|
|
||||||
if item_code:
|
if item_code:
|
||||||
item_cache = ItemVariantsCacheManager(item_code)
|
item_cache = ItemVariantsCacheManager(item_code)
|
||||||
item_cache.clear_cache()
|
item_cache.rebuild_cache()
|
||||||
|
|
||||||
|
|
||||||
def check_stock_uom_with_bin(item, stock_uom):
|
def check_stock_uom_with_bin(item, stock_uom):
|
||||||
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
|
if stock_uom == frappe.db.get_value("Item", item, "stock_uom"):
|
||||||
|
|||||||
@@ -1,357 +1,97 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_events_in_timeline": 0,
|
|
||||||
"allow_guest_to_view": 0,
|
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "field:attribute_name",
|
"autoname": "field:attribute_name",
|
||||||
"beta": 0,
|
|
||||||
"creation": "2014-09-26 03:49:54.899170",
|
"creation": "2014-09-26 03:49:54.899170",
|
||||||
"custom": 0,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
"editable_grid": 0,
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"attribute_name",
|
||||||
|
"numeric_values",
|
||||||
|
"section_break_4",
|
||||||
|
"from_range",
|
||||||
|
"increment",
|
||||||
|
"column_break_8",
|
||||||
|
"to_range",
|
||||||
|
"section_break_5",
|
||||||
|
"item_attribute_values"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "attribute_name",
|
"fieldname": "attribute_name",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Attribute Name",
|
"label": "Attribute Name",
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
"reqd": 1,
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 1
|
"unique": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"fieldname": "numeric_values",
|
"fieldname": "numeric_values",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"hidden": 0,
|
"label": "Numeric Values"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Numeric Values",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "numeric_values",
|
"depends_on": "numeric_values",
|
||||||
"fieldname": "section_break_4",
|
"fieldname": "section_break_4",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "",
|
|
||||||
"fieldname": "from_range",
|
"fieldname": "from_range",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 0,
|
"label": "From Range"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "From Range",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "",
|
|
||||||
"fieldname": "increment",
|
"fieldname": "increment",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 0,
|
"label": "Increment"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Increment",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_8",
|
"fieldname": "column_break_8",
|
||||||
"fieldtype": "Column Break",
|
"fieldtype": "Column Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
"default": "0",
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "",
|
|
||||||
"fieldname": "to_range",
|
"fieldname": "to_range",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hidden": 0,
|
"label": "To Range"
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "To Range",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "eval: !doc.numeric_values",
|
"depends_on": "eval: !doc.numeric_values",
|
||||||
"fieldname": "section_break_5",
|
"fieldname": "section_break_5",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break"
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_bulk_edit": 0,
|
|
||||||
"allow_in_quick_entry": 0,
|
|
||||||
"allow_on_submit": 0,
|
|
||||||
"bold": 0,
|
|
||||||
"collapsible": 0,
|
|
||||||
"columns": 0,
|
|
||||||
"depends_on": "",
|
|
||||||
"fieldname": "item_attribute_values",
|
"fieldname": "item_attribute_values",
|
||||||
"fieldtype": "Table",
|
"fieldtype": "Table",
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Attribute Values",
|
"label": "Item Attribute Values",
|
||||||
"length": 0,
|
"options": "Item Attribute Value"
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Item Attribute Value",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"translatable": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"has_web_view": 0,
|
|
||||||
"hide_heading": 0,
|
|
||||||
"hide_toolbar": 0,
|
|
||||||
"icon": "fa fa-edit",
|
"icon": "fa fa-edit",
|
||||||
"idx": 0,
|
"index_web_pages_for_search": 1,
|
||||||
"image_view": 0,
|
"links": [],
|
||||||
"in_create": 0,
|
"modified": "2020-10-02 12:03:02.359202",
|
||||||
"is_submittable": 0,
|
|
||||||
"issingle": 0,
|
|
||||||
"istable": 0,
|
|
||||||
"max_attachments": 0,
|
|
||||||
"modified": "2019-01-01 13:17:46.524806",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item Attribute",
|
"name": "Item Attribute",
|
||||||
"name_case": "",
|
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
"amend": 0,
|
|
||||||
"cancel": 0,
|
|
||||||
"create": 1,
|
"create": 1,
|
||||||
"delete": 1,
|
"delete": 1,
|
||||||
"email": 0,
|
|
||||||
"export": 0,
|
|
||||||
"if_owner": 0,
|
|
||||||
"import": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print": 0,
|
|
||||||
"read": 1,
|
"read": 1,
|
||||||
"report": 1,
|
"report": 1,
|
||||||
"role": "Item Manager",
|
"role": "Item Manager",
|
||||||
"set_user_permissions": 0,
|
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"submit": 0,
|
|
||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1,
|
"track_changes": 1
|
||||||
"track_seen": 0,
|
|
||||||
"track_views": 0
|
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.utils import flt
|
||||||
|
|
||||||
from erpnext.controllers.item_variant import (validate_is_incremental,
|
from erpnext.controllers.item_variant import (validate_is_incremental,
|
||||||
validate_item_attribute_value, InvalidItemAttributeValueError)
|
validate_item_attribute_value, InvalidItemAttributeValueError)
|
||||||
@@ -42,7 +43,7 @@ class ItemAttribute(Document):
|
|||||||
if self.from_range is None or self.to_range is None:
|
if self.from_range is None or self.to_range is None:
|
||||||
frappe.throw(_("Please specify from/to range"))
|
frappe.throw(_("Please specify from/to range"))
|
||||||
|
|
||||||
elif self.from_range >= self.to_range:
|
elif flt(self.from_range) >= flt(self.to_range):
|
||||||
frappe.throw(_("From Range has to be less than To Range"))
|
frappe.throw(_("From Range has to be less than To Range"))
|
||||||
|
|
||||||
if not self.increment:
|
if not self.increment:
|
||||||
|
|||||||
@@ -50,16 +50,18 @@ class ItemPrice(Document):
|
|||||||
|
|
||||||
def check_duplicates(self):
|
def check_duplicates(self):
|
||||||
conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s"
|
conditions = "where item_code=%(item_code)s and price_list=%(price_list)s and name != %(name)s"
|
||||||
|
condition_data_dict = dict(item_code=self.item_code, price_list=self.price_list, name=self.name)
|
||||||
|
|
||||||
for field in ['uom', 'valid_from',
|
for field in ['uom', 'valid_from',
|
||||||
'valid_upto', 'packing_unit', 'customer', 'supplier']:
|
'valid_upto', 'packing_unit', 'customer', 'supplier']:
|
||||||
if self.get(field):
|
if self.get(field):
|
||||||
conditions += " and {0} = %({1})s".format(field, field)
|
conditions += " and {0} = %({1})s".format(field, field)
|
||||||
|
condition_data_dict[field] = self.get(field)
|
||||||
|
|
||||||
price_list_rate = frappe.db.sql("""
|
price_list_rate = frappe.db.sql("""
|
||||||
SELECT price_list_rate
|
SELECT price_list_rate
|
||||||
FROM `tabItem Price`
|
FROM `tabItem Price`
|
||||||
{conditions} """.format(conditions=conditions), self.as_dict())
|
{conditions} """.format(conditions=conditions), condition_data_dict)
|
||||||
|
|
||||||
if price_list_rate :
|
if price_list_rate :
|
||||||
frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem)
|
frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty and Dates."), ItemPriceDuplicateItem)
|
||||||
|
|||||||
@@ -207,7 +207,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
from erpnext.accounts.general_ledger import process_gl_map
|
from erpnext.accounts.general_ledger import process_gl_map
|
||||||
|
|
||||||
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
|
||||||
cogs_account = self.get_company_default("default_expense_account")
|
stock_rbnb_currency = get_account_currency(stock_rbnb)
|
||||||
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
|
||||||
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
|
||||||
|
|
||||||
@@ -243,7 +243,6 @@ class PurchaseReceipt(BuyingController):
|
|||||||
|
|
||||||
# stock received but not billed
|
# stock received but not billed
|
||||||
if d.base_net_amount:
|
if d.base_net_amount:
|
||||||
stock_rbnb_currency = get_account_currency(stock_rbnb)
|
|
||||||
gl_entries.append(self.get_gl_dict({
|
gl_entries.append(self.get_gl_dict({
|
||||||
"account": stock_rbnb,
|
"account": stock_rbnb,
|
||||||
"against": warehouse_account[d.warehouse]["account"],
|
"against": warehouse_account[d.warehouse]["account"],
|
||||||
@@ -289,6 +288,7 @@ class PurchaseReceipt(BuyingController):
|
|||||||
if self.is_return or flt(d.item_tax_amount):
|
if self.is_return or flt(d.item_tax_amount):
|
||||||
loss_account = expenses_included_in_valuation
|
loss_account = expenses_included_in_valuation
|
||||||
else:
|
else:
|
||||||
|
cogs_account = self.get_company_default("default_expense_account")
|
||||||
loss_account = cogs_account
|
loss_account = cogs_account
|
||||||
|
|
||||||
gl_entries.append(self.get_gl_dict({
|
gl_entries.append(self.get_gl_dict({
|
||||||
|
|||||||
@@ -471,8 +471,7 @@ class TestPurchaseReceipt(unittest.TestCase):
|
|||||||
"expected_value_after_useful_life": 10,
|
"expected_value_after_useful_life": 10,
|
||||||
"depreciation_method": "Straight Line",
|
"depreciation_method": "Straight Line",
|
||||||
"total_number_of_depreciations": 3,
|
"total_number_of_depreciations": 3,
|
||||||
"frequency_of_depreciation": 1,
|
"frequency_of_depreciation": 1
|
||||||
"depreciation_start_date": frappe.utils.nowdate()
|
|
||||||
})
|
})
|
||||||
asset.submit()
|
asset.submit()
|
||||||
|
|
||||||
@@ -614,9 +613,9 @@ class TestPurchaseReceipt(unittest.TestCase):
|
|||||||
|
|
||||||
rm_items = [
|
rm_items = [
|
||||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
|
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
|
||||||
"qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"},
|
"qty":300,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name},
|
||||||
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
|
{"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 3","item_name":"_Test Item",
|
||||||
"qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}
|
"qty":200,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos", "name": po.supplied_items[0].name}
|
||||||
]
|
]
|
||||||
|
|
||||||
rm_item_string = json.dumps(rm_items)
|
rm_item_string = json.dumps(rm_items)
|
||||||
|
|||||||
@@ -24,6 +24,24 @@ frappe.ui.form.on('Stock Entry', {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
frm.set_query('source_warehouse_address', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
link_doctype: 'Warehouse',
|
||||||
|
link_name: frm.doc.from_warehouse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
frm.set_query('target_warehouse_address', function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
link_doctype: 'Warehouse',
|
||||||
|
link_name: frm.doc.to_warehouse
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => {
|
frappe.db.get_value('Stock Settings', {name: 'Stock Settings'}, 'sample_retention_warehouse', (r) => {
|
||||||
if (r.sample_retention_warehouse) {
|
if (r.sample_retention_warehouse) {
|
||||||
var filters = [
|
var filters = [
|
||||||
@@ -139,6 +157,7 @@ frappe.ui.form.on('Stock Entry', {
|
|||||||
mr_item.item_code = item.item_code;
|
mr_item.item_code = item.item_code;
|
||||||
mr_item.item_name = item.item_name;
|
mr_item.item_name = item.item_name;
|
||||||
mr_item.uom = item.uom;
|
mr_item.uom = item.uom;
|
||||||
|
mr_item.stock_uom = item.stock_uom;
|
||||||
mr_item.conversion_factor = item.conversion_factor;
|
mr_item.conversion_factor = item.conversion_factor;
|
||||||
mr_item.item_group = item.item_group;
|
mr_item.item_group = item.item_group;
|
||||||
mr_item.description = item.description;
|
mr_item.description = item.description;
|
||||||
|
|||||||
@@ -844,6 +844,8 @@ class StockEntry(StockController):
|
|||||||
frappe.throw(_("Posting date and posting time is mandatory"))
|
frappe.throw(_("Posting date and posting time is mandatory"))
|
||||||
|
|
||||||
self.set_work_order_details()
|
self.set_work_order_details()
|
||||||
|
self.flags.backflush_based_on = frappe.db.get_single_value("Manufacturing Settings",
|
||||||
|
"backflush_raw_materials_based_on")
|
||||||
|
|
||||||
if self.bom_no:
|
if self.bom_no:
|
||||||
|
|
||||||
@@ -857,14 +859,14 @@ class StockEntry(StockController):
|
|||||||
item["to_warehouse"] = self.pro_doc.wip_warehouse
|
item["to_warehouse"] = self.pro_doc.wip_warehouse
|
||||||
self.add_to_stock_entry_detail(item_dict)
|
self.add_to_stock_entry_detail(item_dict)
|
||||||
|
|
||||||
elif (self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture")
|
elif (self.work_order and (self.purpose == "Manufacture"
|
||||||
and not self.pro_doc.skip_transfer and frappe.db.get_single_value("Manufacturing Settings",
|
or self.purpose == "Material Consumption for Manufacture") and not self.pro_doc.skip_transfer
|
||||||
"backflush_raw_materials_based_on")== "Material Transferred for Manufacture"):
|
and self.flags.backflush_based_on == "Material Transferred for Manufacture"):
|
||||||
self.get_transfered_raw_materials()
|
self.get_transfered_raw_materials()
|
||||||
|
|
||||||
elif self.work_order and (self.purpose == "Manufacture" or self.purpose == "Material Consumption for Manufacture") and \
|
elif (self.work_order and (self.purpose == "Manufacture" or
|
||||||
frappe.db.get_single_value("Manufacturing Settings", "backflush_raw_materials_based_on")== "BOM" and \
|
self.purpose == "Material Consumption for Manufacture") and self.flags.backflush_based_on== "BOM"
|
||||||
frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1:
|
and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")== 1):
|
||||||
self.get_unconsumed_raw_materials()
|
self.get_unconsumed_raw_materials()
|
||||||
else:
|
else:
|
||||||
if not self.fg_completed_qty:
|
if not self.fg_completed_qty:
|
||||||
@@ -1108,7 +1110,6 @@ class StockEntry(StockController):
|
|||||||
for d in backflushed_materials.get(item.item_code):
|
for d in backflushed_materials.get(item.item_code):
|
||||||
if d.get(item.warehouse):
|
if d.get(item.warehouse):
|
||||||
if (qty > req_qty):
|
if (qty > req_qty):
|
||||||
qty = req_qty
|
|
||||||
qty-= d.get(item.warehouse)
|
qty-= d.get(item.warehouse)
|
||||||
|
|
||||||
if qty > 0:
|
if qty > 0:
|
||||||
@@ -1133,11 +1134,22 @@ class StockEntry(StockController):
|
|||||||
"""
|
"""
|
||||||
item_dict = self.get_pro_order_required_items()
|
item_dict = self.get_pro_order_required_items()
|
||||||
max_qty = flt(self.pro_doc.qty)
|
max_qty = flt(self.pro_doc.qty)
|
||||||
|
|
||||||
|
allow_overproduction = False
|
||||||
|
overproduction_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
|
||||||
|
"overproduction_percentage_for_work_order"))
|
||||||
|
|
||||||
|
to_transfer_qty = flt(self.pro_doc.material_transferred_for_manufacturing) + flt(self.fg_completed_qty)
|
||||||
|
transfer_limit_qty = max_qty + ((max_qty * overproduction_percentage) / 100)
|
||||||
|
|
||||||
|
if transfer_limit_qty >= to_transfer_qty:
|
||||||
|
allow_overproduction = True
|
||||||
|
|
||||||
for item, item_details in iteritems(item_dict):
|
for item, item_details in iteritems(item_dict):
|
||||||
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
|
pending_to_issue = flt(item_details.required_qty) - flt(item_details.transferred_qty)
|
||||||
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty
|
desire_to_transfer = flt(self.fg_completed_qty) * flt(item_details.required_qty) / max_qty
|
||||||
|
|
||||||
if desire_to_transfer <= pending_to_issue:
|
if desire_to_transfer <= pending_to_issue or allow_overproduction:
|
||||||
item_dict[item]["qty"] = desire_to_transfer
|
item_dict[item]["qty"] = desire_to_transfer
|
||||||
elif pending_to_issue > 0:
|
elif pending_to_issue > 0:
|
||||||
item_dict[item]["qty"] = pending_to_issue
|
item_dict[item]["qty"] = pending_to_issue
|
||||||
@@ -1255,8 +1267,9 @@ class StockEntry(StockController):
|
|||||||
FROM
|
FROM
|
||||||
`tabStock Entry Detail` sed, `tabStock Entry` se
|
`tabStock Entry Detail` sed, `tabStock Entry` se
|
||||||
WHERE
|
WHERE
|
||||||
(pos.name = sed.po_detail OR sed.subcontracted_item = pos.main_item_code)
|
pos.name = sed.po_detail AND pos.rm_item_code = sed.item_code
|
||||||
AND sed.docstatus = 1 AND se.name = sed.parent and se.purchase_order = %(po)s
|
AND pos.parent = se.purchase_order AND sed.docstatus = 1
|
||||||
|
AND se.name = sed.parent and se.purchase_order = %(po)s
|
||||||
), 0)
|
), 0)
|
||||||
WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})
|
WHERE pos.docstatus = 1 and pos.parent = %(po)s""", {"po": self.purchase_order})
|
||||||
|
|
||||||
|
|||||||
@@ -109,6 +109,10 @@ frappe.ui.form.on("Stock Reconciliation", {
|
|||||||
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
|
frappe.model.set_value(cdt, cdn, "current_amount", r.message.rate * r.message.qty);
|
||||||
frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty);
|
frappe.model.set_value(cdt, cdn, "amount", r.message.rate * r.message.qty);
|
||||||
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
|
frappe.model.set_value(cdt, cdn, "current_serial_no", r.message.serial_nos);
|
||||||
|
|
||||||
|
if (frm.doc.purpose == "Stock Reconciliation" && !d.serial_no) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "serial_no", r.message.serial_nos);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -183,6 +187,11 @@ frappe.ui.form.on("Stock Reconciliation Item", {
|
|||||||
frappe.model.set_value(cdt, cdn, "batch_no", "");
|
frappe.model.set_value(cdt, cdn, "batch_no", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (child.serial_no) {
|
||||||
|
frappe.model.set_value(cdt, cdn, "serial_no", "");
|
||||||
|
frappe.model.set_value(cdt, cdn, "current_serial_no", "");
|
||||||
|
}
|
||||||
|
|
||||||
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
|
frm.events.set_valuation_rate_and_qty(frm, cdt, cdn);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ class StockReconciliation(StockController):
|
|||||||
|
|
||||||
if item_dict.get("serial_nos"):
|
if item_dict.get("serial_nos"):
|
||||||
item.current_serial_no = item_dict.get("serial_nos")
|
item.current_serial_no = item_dict.get("serial_nos")
|
||||||
|
if self.purpose == "Stock Reconciliation" and not item.serial_no:
|
||||||
|
item.serial_no = item.current_serial_no
|
||||||
|
|
||||||
item.current_qty = item_dict.get("qty")
|
item.current_qty = item_dict.get("qty")
|
||||||
item.current_valuation_rate = item_dict.get("rate")
|
item.current_valuation_rate = item_dict.get("rate")
|
||||||
@@ -172,8 +174,9 @@ class StockReconciliation(StockController):
|
|||||||
row.serial_no = ''
|
row.serial_no = ''
|
||||||
|
|
||||||
# item managed batch-wise not allowed
|
# item managed batch-wise not allowed
|
||||||
if item.has_batch_no and not row.batch_no and not item.create_new_batch:
|
if item.has_batch_no and not row.batch_no and not frappe.flags.in_test:
|
||||||
raise frappe.ValidationError(_("Batch no is required for batched item {0}").format(item_code))
|
if not item.create_new_batch or self.purpose != 'Opening Stock':
|
||||||
|
raise frappe.ValidationError(_("Batch no is required for the batched item {0}").format(item_code))
|
||||||
|
|
||||||
# docstatus should be < 2
|
# docstatus should be < 2
|
||||||
validate_cancelled_item(item_code, item.docstatus, verbose=0)
|
validate_cancelled_item(item_code, item.docstatus, verbose=0)
|
||||||
@@ -191,10 +194,11 @@ class StockReconciliation(StockController):
|
|||||||
serialized_items = False
|
serialized_items = False
|
||||||
for row in self.items:
|
for row in self.items:
|
||||||
item = frappe.get_cached_doc("Item", row.item_code)
|
item = frappe.get_cached_doc("Item", row.item_code)
|
||||||
if not (item.has_serial_no or item.has_batch_no):
|
if not (item.has_serial_no):
|
||||||
if row.serial_no or row.batch_no:
|
if row.serial_no:
|
||||||
frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \
|
frappe.throw(_("Row #{0}: Item {1} is not a Serialized/Batched Item. It cannot have a Serial No/Batch No against it.") \
|
||||||
.format(row.idx, frappe.bold(row.item_code)))
|
.format(row.idx, frappe.bold(row.item_code)))
|
||||||
|
|
||||||
previous_sle = get_previous_sle({
|
previous_sle = get_previous_sle({
|
||||||
"item_code": row.item_code,
|
"item_code": row.item_code,
|
||||||
"warehouse": row.warehouse,
|
"warehouse": row.warehouse,
|
||||||
@@ -217,7 +221,12 @@ class StockReconciliation(StockController):
|
|||||||
or (not previous_sle and not row.qty)):
|
or (not previous_sle and not row.qty)):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
sl_entries.append(self.get_sle_for_items(row))
|
sle_data = self.get_sle_for_items(row)
|
||||||
|
|
||||||
|
if row.batch_no:
|
||||||
|
sle_data.actual_qty = row.quantity_difference
|
||||||
|
|
||||||
|
sl_entries.append(sle_data)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
serialized_items = True
|
serialized_items = True
|
||||||
@@ -244,7 +253,7 @@ class StockReconciliation(StockController):
|
|||||||
serial_nos = get_serial_nos(row.serial_no) or []
|
serial_nos = get_serial_nos(row.serial_no) or []
|
||||||
|
|
||||||
# To issue existing serial nos
|
# To issue existing serial nos
|
||||||
if row.current_qty and (row.current_serial_no or row.batch_no):
|
if row.current_qty and (row.current_serial_no):
|
||||||
args = self.get_sle_for_items(row)
|
args = self.get_sle_for_items(row)
|
||||||
args.update({
|
args.update({
|
||||||
'actual_qty': -1 * row.current_qty,
|
'actual_qty': -1 * row.current_qty,
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
to_delete_records.append(sr.name)
|
to_delete_records.append(sr.name)
|
||||||
|
|
||||||
sr = create_stock_reconciliation(item_code=serial_item_code,
|
sr = create_stock_reconciliation(item_code=serial_item_code,
|
||||||
warehouse = serial_warehouse, qty=5, rate=300, serial_no = '\n'.join(serial_nos))
|
warehouse = serial_warehouse, qty=5, rate=300)
|
||||||
|
|
||||||
# print(sr.name)
|
# print(sr.name)
|
||||||
serial_nos1 = get_serial_nos(sr.items[0].serial_no)
|
serial_nos1 = get_serial_nos(sr.items[0].serial_no)
|
||||||
@@ -361,6 +361,37 @@ class TestStockReconciliation(unittest.TestCase):
|
|||||||
doc.cancel()
|
doc.cancel()
|
||||||
frappe.delete_doc(doc.doctype, doc.name)
|
frappe.delete_doc(doc.doctype, doc.name)
|
||||||
|
|
||||||
|
def test_allow_negative_for_batch(self):
|
||||||
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
item_code = "Stock-Reco-batch-Item-5"
|
||||||
|
warehouse = "_Test Warehouse for Stock Reco5 - _TC"
|
||||||
|
|
||||||
|
create_warehouse("_Test Warehouse for Stock Reco5", {"is_group": 0,
|
||||||
|
"parent_warehouse": "_Test Warehouse Group - _TC", "company": "_Test Company"})
|
||||||
|
|
||||||
|
batch_item_doc = create_item(item_code, is_stock_item=1)
|
||||||
|
if not batch_item_doc.has_batch_no:
|
||||||
|
frappe.db.set_value("Item", item_code, {
|
||||||
|
"has_batch_no": 1,
|
||||||
|
"create_new_batch": 1,
|
||||||
|
"batch_number_series": "Test-C.####"
|
||||||
|
})
|
||||||
|
|
||||||
|
ste1=make_stock_entry(posting_date="2020-10-07", posting_time="02:00", item_code=item_code,
|
||||||
|
target=warehouse, qty=2, basic_rate=100)
|
||||||
|
|
||||||
|
batch_no = ste1.items[0].batch_no
|
||||||
|
|
||||||
|
ste2=make_stock_entry(posting_date="2020-10-09", posting_time="02:00", item_code=item_code,
|
||||||
|
source=warehouse, qty=2, basic_rate=100, batch_no=batch_no)
|
||||||
|
|
||||||
|
sr = create_stock_reconciliation(item_code=item_code,
|
||||||
|
warehouse = warehouse, batch_no=batch_no, rate=200)
|
||||||
|
|
||||||
|
for doc in [sr, ste2, ste1]:
|
||||||
|
doc.cancel()
|
||||||
|
frappe.delete_doc(doc.doctype, doc.name)
|
||||||
|
|
||||||
def insert_existing_sle(warehouse):
|
def insert_existing_sle(warehouse):
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||||
|
|
||||||
|
|||||||
@@ -385,6 +385,11 @@ def get_item_warehouse(item, args, overwrite_warehouse, defaults={}):
|
|||||||
else:
|
else:
|
||||||
warehouse = args.get('warehouse')
|
warehouse = args.get('warehouse')
|
||||||
|
|
||||||
|
if not warehouse:
|
||||||
|
default_warehouse = frappe.db.get_single_value("Stock Settings", "default_warehouse")
|
||||||
|
if frappe.db.get_value("Warehouse", default_warehouse, "company") == args.company:
|
||||||
|
return default_warehouse
|
||||||
|
|
||||||
return warehouse
|
return warehouse
|
||||||
|
|
||||||
def update_barcode_value(out):
|
def update_barcode_value(out):
|
||||||
|
|||||||
@@ -3,19 +3,70 @@
|
|||||||
|
|
||||||
frappe.query_reports["Batch-Wise Balance History"] = {
|
frappe.query_reports["Batch-Wise Balance History"] = {
|
||||||
"filters": [
|
"filters": [
|
||||||
|
{
|
||||||
|
"fieldname":"company",
|
||||||
|
"label": __("Company"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"default": frappe.defaults.get_user_default("Company"),
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"from_date",
|
"fieldname":"from_date",
|
||||||
"label": __("From Date"),
|
"label": __("From Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"width": "80",
|
"width": "80",
|
||||||
"default": frappe.sys_defaults.year_start_date,
|
"default": frappe.sys_defaults.year_start_date,
|
||||||
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"to_date",
|
"fieldname":"to_date",
|
||||||
"label": __("To Date"),
|
"label": __("To Date"),
|
||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"width": "80",
|
"width": "80",
|
||||||
"default": frappe.datetime.get_today()
|
"default": frappe.datetime.get_today(),
|
||||||
}
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"item_code",
|
||||||
|
"label": __("Item Code"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Item",
|
||||||
|
"get_query": function() {
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
"has_batch_no": 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"warehouse",
|
||||||
|
"label": __("Warehouse"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Warehouse",
|
||||||
|
"get_query": function() {
|
||||||
|
let company = frappe.query_report.get_filter_value('company');
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
"company": company
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"batch_no",
|
||||||
|
"label": __("Batch No"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Batch",
|
||||||
|
"get_query": function() {
|
||||||
|
let item_code = frappe.query_report.get_filter_value('item_code');
|
||||||
|
return {
|
||||||
|
filters: {
|
||||||
|
"item": item_code
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,9 @@ from frappe.utils import flt, cint, getdate
|
|||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
if not filters: filters = {}
|
if not filters: filters = {}
|
||||||
|
|
||||||
|
if filters.from_date > filters.to_date:
|
||||||
|
frappe.throw(_("From Date must be before To Date"))
|
||||||
|
|
||||||
float_precision = cint(frappe.db.get_default("float_precision")) or 3
|
float_precision = cint(frappe.db.get_default("float_precision")) or 3
|
||||||
|
|
||||||
columns = get_columns(filters)
|
columns = get_columns(filters)
|
||||||
@@ -50,6 +53,10 @@ def get_conditions(filters):
|
|||||||
else:
|
else:
|
||||||
frappe.throw(_("'To Date' is required"))
|
frappe.throw(_("'To Date' is required"))
|
||||||
|
|
||||||
|
for field in ["item_code", "warehouse", "batch_no", "company"]:
|
||||||
|
if filters.get(field):
|
||||||
|
conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field)))
|
||||||
|
|
||||||
return conditions
|
return conditions
|
||||||
|
|
||||||
#get all details
|
#get all details
|
||||||
|
|||||||
@@ -77,38 +77,33 @@ def get_price_list():
|
|||||||
return item_rate_map
|
return item_rate_map
|
||||||
|
|
||||||
def get_last_purchase_rate():
|
def get_last_purchase_rate():
|
||||||
|
|
||||||
item_last_purchase_rate_map = {}
|
item_last_purchase_rate_map = {}
|
||||||
|
|
||||||
query = """select * from (select
|
query = """select * from (
|
||||||
result.item_code,
|
(select
|
||||||
result.base_rate
|
po_item.item_code,
|
||||||
from (
|
po.transaction_date as posting_date,
|
||||||
(select
|
po_item.base_rate
|
||||||
po_item.item_code,
|
from `tabPurchase Order` po, `tabPurchase Order Item` po_item
|
||||||
po_item.item_name,
|
where po.name = po_item.parent and po.docstatus = 1)
|
||||||
po.transaction_date as posting_date,
|
union
|
||||||
po_item.base_price_list_rate,
|
(select
|
||||||
po_item.discount_percentage,
|
pr_item.item_code,
|
||||||
po_item.base_rate
|
pr.posting_date,
|
||||||
from `tabPurchase Order` po, `tabPurchase Order Item` po_item
|
pr_item.base_rate
|
||||||
where po.name = po_item.parent and po.docstatus = 1)
|
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
|
||||||
union
|
where pr.name = pr_item.parent and pr.docstatus = 1)
|
||||||
(select
|
union
|
||||||
pr_item.item_code,
|
(select
|
||||||
pr_item.item_name,
|
pi_item.item_code,
|
||||||
pr.posting_date,
|
pi.posting_date,
|
||||||
pr_item.base_price_list_rate,
|
pi_item.base_rate
|
||||||
pr_item.discount_percentage,
|
from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pi_item
|
||||||
pr_item.base_rate
|
where pi.name = pi_item.parent and pi.docstatus = 1 and pi.update_stock = 1)
|
||||||
from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item
|
) result order by result.item_code asc, result.posting_date asc"""
|
||||||
where pr.name = pr_item.parent and pr.docstatus = 1)
|
|
||||||
) result
|
|
||||||
order by result.item_code asc, result.posting_date desc) result_wrapper
|
|
||||||
group by item_code"""
|
|
||||||
|
|
||||||
for d in frappe.db.sql(query, as_dict=1):
|
for d in frappe.db.sql(query, as_dict=1):
|
||||||
item_last_purchase_rate_map.setdefault(d.item_code, d.base_rate)
|
item_last_purchase_rate_map[d.item_code] = d.base_rate
|
||||||
|
|
||||||
return item_last_purchase_rate_map
|
return item_last_purchase_rate_map
|
||||||
|
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ def execute(filters=None):
|
|||||||
|
|
||||||
data = []
|
data = []
|
||||||
for item, item_dict in iteritems(item_details):
|
for item, item_dict in iteritems(item_details):
|
||||||
|
earliest_age, latest_age = 0, 0
|
||||||
|
|
||||||
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
|
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
|
||||||
details = item_dict["details"]
|
details = item_dict["details"]
|
||||||
if not fifo_queue or (not item_dict.get("total_qty")): continue
|
if not fifo_queue and (not item_dict.get("total_qty")): continue
|
||||||
|
|
||||||
average_age = get_average_age(fifo_queue, to_date)
|
average_age = get_average_age(fifo_queue, to_date)
|
||||||
earliest_age = date_diff(to_date, fifo_queue[0][1])
|
|
||||||
latest_age = date_diff(to_date, fifo_queue[-1][1])
|
if fifo_queue:
|
||||||
|
earliest_age = date_diff(to_date, fifo_queue[0][1])
|
||||||
|
latest_age = date_diff(to_date, fifo_queue[-1][1])
|
||||||
|
|
||||||
row = [details.name, details.item_name,
|
row = [details.name, details.item_name,
|
||||||
details.description, details.item_group, details.brand]
|
details.description, details.item_group, details.brand]
|
||||||
@@ -147,7 +150,8 @@ def get_fifo_queue(filters, sle=None):
|
|||||||
item_details.setdefault(key, {"details": d, "fifo_queue": []})
|
item_details.setdefault(key, {"details": d, "fifo_queue": []})
|
||||||
fifo_queue = item_details[key]["fifo_queue"]
|
fifo_queue = item_details[key]["fifo_queue"]
|
||||||
|
|
||||||
transferred_item_details.setdefault((d.voucher_no, d.name), [])
|
transferred_item_key = (d.voucher_no, d.name, d.warehouse)
|
||||||
|
transferred_item_details.setdefault(transferred_item_key, [])
|
||||||
|
|
||||||
if d.voucher_type == "Stock Reconciliation":
|
if d.voucher_type == "Stock Reconciliation":
|
||||||
d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0))
|
d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0))
|
||||||
@@ -155,10 +159,10 @@ def get_fifo_queue(filters, sle=None):
|
|||||||
serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else []
|
serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else []
|
||||||
|
|
||||||
if d.actual_qty > 0:
|
if d.actual_qty > 0:
|
||||||
if transferred_item_details.get((d.voucher_no, d.name)):
|
if transferred_item_details.get(transferred_item_key):
|
||||||
batch = transferred_item_details[(d.voucher_no, d.name)][0]
|
batch = transferred_item_details[transferred_item_key][0]
|
||||||
fifo_queue.append(batch)
|
fifo_queue.append(batch)
|
||||||
transferred_item_details[((d.voucher_no, d.name))].pop(0)
|
transferred_item_details[transferred_item_key].pop(0)
|
||||||
else:
|
else:
|
||||||
if serial_no_list:
|
if serial_no_list:
|
||||||
for serial_no in serial_no_list:
|
for serial_no in serial_no_list:
|
||||||
@@ -182,11 +186,11 @@ def get_fifo_queue(filters, sle=None):
|
|||||||
# if batch qty > 0
|
# if batch qty > 0
|
||||||
# not enough or exactly same qty in current batch, clear batch
|
# not enough or exactly same qty in current batch, clear batch
|
||||||
qty_to_pop -= flt(batch[0])
|
qty_to_pop -= flt(batch[0])
|
||||||
transferred_item_details[(d.voucher_no, d.name)].append(fifo_queue.pop(0))
|
transferred_item_details[transferred_item_key].append(fifo_queue.pop(0))
|
||||||
else:
|
else:
|
||||||
# all from current batch
|
# all from current batch
|
||||||
batch[0] = flt(batch[0]) - qty_to_pop
|
batch[0] = flt(batch[0]) - qty_to_pop
|
||||||
transferred_item_details[(d.voucher_no, d.name)].append([qty_to_pop, batch[1]])
|
transferred_item_details[transferred_item_key].append([qty_to_pop, batch[1]])
|
||||||
qty_to_pop = 0
|
qty_to_pop = 0
|
||||||
|
|
||||||
item_details[key]["qty_after_transaction"] = d.qty_after_transaction
|
item_details[key]["qty_after_transaction"] = d.qty_after_transaction
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt
|
||||||
from erpnext.stock.utils import update_included_uom_in_report
|
from erpnext.stock.utils import update_included_uom_in_report
|
||||||
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters=None):
|
||||||
include_uom = filters.get("include_uom")
|
include_uom = filters.get("include_uom")
|
||||||
@@ -23,6 +24,7 @@ def execute(filters=None):
|
|||||||
|
|
||||||
actual_qty = stock_value = 0
|
actual_qty = stock_value = 0
|
||||||
|
|
||||||
|
available_serial_nos = {}
|
||||||
for sle in sl_entries:
|
for sle in sl_entries:
|
||||||
item_detail = item_details[sle.item_code]
|
item_detail = item_details[sle.item_code]
|
||||||
|
|
||||||
@@ -41,6 +43,9 @@ def execute(filters=None):
|
|||||||
"stock_value": stock_value
|
"stock_value": stock_value
|
||||||
})
|
})
|
||||||
|
|
||||||
|
if sle.serial_no:
|
||||||
|
update_available_serial_nos(available_serial_nos, sle)
|
||||||
|
|
||||||
data.append(sle)
|
data.append(sle)
|
||||||
|
|
||||||
if include_uom:
|
if include_uom:
|
||||||
@@ -49,6 +54,27 @@ def execute(filters=None):
|
|||||||
update_included_uom_in_report(columns, data, include_uom, conversion_factors)
|
update_included_uom_in_report(columns, data, include_uom, conversion_factors)
|
||||||
return columns, data
|
return columns, data
|
||||||
|
|
||||||
|
def update_available_serial_nos(available_serial_nos, sle):
|
||||||
|
serial_nos = get_serial_nos(sle.serial_no)
|
||||||
|
key = (sle.item_code, sle.warehouse)
|
||||||
|
if key not in available_serial_nos:
|
||||||
|
available_serial_nos.setdefault(key, [])
|
||||||
|
|
||||||
|
existing_serial_no = available_serial_nos[key]
|
||||||
|
for sn in serial_nos:
|
||||||
|
if sle.actual_qty > 0:
|
||||||
|
if sn in existing_serial_no:
|
||||||
|
existing_serial_no.remove(sn)
|
||||||
|
else:
|
||||||
|
existing_serial_no.append(sn)
|
||||||
|
else:
|
||||||
|
if sn in existing_serial_no:
|
||||||
|
existing_serial_no.remove(sn)
|
||||||
|
else:
|
||||||
|
existing_serial_no.append(sn)
|
||||||
|
|
||||||
|
sle.balance_serial_no = '\n'.join(existing_serial_no)
|
||||||
|
|
||||||
def get_columns():
|
def get_columns():
|
||||||
columns = [
|
columns = [
|
||||||
{"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95},
|
{"label": _("Date"), "fieldname": "date", "fieldtype": "Datetime", "width": 95},
|
||||||
@@ -70,7 +96,8 @@ def get_columns():
|
|||||||
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
|
{"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 110},
|
||||||
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
|
{"label": _("Voucher #"), "fieldname": "voucher_no", "fieldtype": "Dynamic Link", "options": "voucher_type", "width": 100},
|
||||||
{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},
|
{"label": _("Batch"), "fieldname": "batch_no", "fieldtype": "Link", "options": "Batch", "width": 100},
|
||||||
{"label": _("Serial #"), "fieldname": "serial_no", "width": 100},
|
{"label": _("Serial No"), "fieldname": "serial_no", "width": 100},
|
||||||
|
{"label": _("Balance Serial No"), "fieldname": "balance_serial_no", "width": 100},
|
||||||
{"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
|
{"label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", "width": 100},
|
||||||
{"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110}
|
{"label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", "width": 110}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -162,10 +162,13 @@ class update_entries_after(object):
|
|||||||
|
|
||||||
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
|
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
|
||||||
else:
|
else:
|
||||||
if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
|
if sle.voucher_type=="Stock Reconciliation":
|
||||||
# assert
|
if sle.batch_no:
|
||||||
|
self.qty_after_transaction += flt(sle.actual_qty)
|
||||||
|
else:
|
||||||
|
self.qty_after_transaction = sle.qty_after_transaction
|
||||||
|
|
||||||
self.valuation_rate = sle.valuation_rate
|
self.valuation_rate = sle.valuation_rate
|
||||||
self.qty_after_transaction = sle.qty_after_transaction
|
|
||||||
self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
|
self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
|
||||||
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
|
self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ frappe
|
|||||||
gocardless-pro==1.11.0
|
gocardless-pro==1.11.0
|
||||||
googlemaps==3.1.1
|
googlemaps==3.1.1
|
||||||
pandas==0.24.2
|
pandas==0.24.2
|
||||||
plaid-python==3.4.0
|
plaid-python==6.0.0
|
||||||
PyGithub==1.44.1
|
PyGithub==1.44.1
|
||||||
python-stdnum==1.12
|
python-stdnum==1.12
|
||||||
Unidecode==1.1.1
|
Unidecode==1.1.1
|
||||||
|
|||||||
Reference in New Issue
Block a user