fix: conflicts while merging brnach version-13 to develop
This commit is contained in:
commit
eb1e3181a4
@ -156,6 +156,6 @@
|
|||||||
"onScan": true,
|
"onScan": true,
|
||||||
"html2canvas": true,
|
"html2canvas": true,
|
||||||
"extend_cscript": true,
|
"extend_cscript": true,
|
||||||
"localforage": true,
|
"localforage": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,6 @@ context('Organizational Chart', () => {
|
|||||||
}).then(res => {
|
}).then(res => {
|
||||||
expect(res.status).eq(200);
|
expect(res.status).eq(200);
|
||||||
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
|
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
|
||||||
|
|
||||||
cy.get('@input')
|
cy.get('@input')
|
||||||
.clear({ force: true })
|
.clear({ force: true })
|
||||||
.type('Test Org Chart{enter}', { force: true })
|
.type('Test Org Chart{enter}', { force: true })
|
||||||
|
@ -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__ = '13.8.0'
|
__version__ = '13.9.0'
|
||||||
|
|
||||||
def get_default_company(user=None):
|
def get_default_company(user=None):
|
||||||
'''Get default company for user'''
|
'''Get default company for user'''
|
||||||
|
@ -275,7 +275,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-09 13:08:01.335416",
|
"modified": "2021-08-09 13:08:04.335416",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts Settings",
|
"name": "Accounts Settings",
|
||||||
|
0
erpnext/accounts/doctype/campaign_item/__init__.py
Normal file
0
erpnext/accounts/doctype/campaign_item/__init__.py
Normal file
31
erpnext/accounts/doctype/campaign_item/campaign_item.json
Normal file
31
erpnext/accounts/doctype/campaign_item/campaign_item.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-06 16:18:25.410476",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"campaign"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "campaign",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Campaign",
|
||||||
|
"options": "Campaign"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-05-07 10:43:49.717633",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Campaign Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
8
erpnext/accounts/doctype/campaign_item/campaign_item.py
Normal file
8
erpnext/accounts/doctype/campaign_item/campaign_item.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class CampaignItem(Document):
|
||||||
|
pass
|
@ -57,7 +57,7 @@ def test_create_test_data():
|
|||||||
})
|
})
|
||||||
item_price.insert()
|
item_price.insert()
|
||||||
# create test item pricing rule
|
# create test item pricing rule
|
||||||
if not frappe.db.exists("Pricing Rule","_Test Pricing Rule for _Test Item"):
|
if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}):
|
||||||
item_pricing_rule = frappe.get_doc({
|
item_pricing_rule = frappe.get_doc({
|
||||||
"doctype": "Pricing Rule",
|
"doctype": "Pricing Rule",
|
||||||
"title": "_Test Pricing Rule for _Test Item",
|
"title": "_Test Pricing Rule for _Test Item",
|
||||||
@ -86,14 +86,15 @@ def test_create_test_data():
|
|||||||
sales_partner.insert()
|
sales_partner.insert()
|
||||||
# create test item coupon code
|
# create test item coupon code
|
||||||
if not frappe.db.exists("Coupon Code", "SAVE30"):
|
if not frappe.db.exists("Coupon Code", "SAVE30"):
|
||||||
|
pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name'])
|
||||||
coupon_code = frappe.get_doc({
|
coupon_code = frappe.get_doc({
|
||||||
"doctype": "Coupon Code",
|
"doctype": "Coupon Code",
|
||||||
"coupon_name":"SAVE30",
|
"coupon_name":"SAVE30",
|
||||||
"coupon_code":"SAVE30",
|
"coupon_code":"SAVE30",
|
||||||
"pricing_rule": "_Test Pricing Rule for _Test Item",
|
"pricing_rule": pricing_rule,
|
||||||
"valid_from": "2014-01-01",
|
"valid_from": "2014-01-01",
|
||||||
"maximum_use":1,
|
"maximum_use":1,
|
||||||
"used":0
|
"used":0
|
||||||
})
|
})
|
||||||
coupon_code.insert()
|
coupon_code.insert()
|
||||||
|
|
||||||
@ -102,7 +103,7 @@ class TestCouponCode(unittest.TestCase):
|
|||||||
test_create_test_data()
|
test_create_test_data()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|
||||||
def test_sales_order_with_coupon_code(self):
|
def test_sales_order_with_coupon_code(self):
|
||||||
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
|
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-06 16:12:42.558878",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"customer_group"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "customer_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Customer Group",
|
||||||
|
"options": "Customer Group"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-05-07 10:39:21.563506",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Customer Group Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class CustomerGroupItem(Document):
|
||||||
|
pass
|
0
erpnext/accounts/doctype/customer_item/__init__.py
Normal file
0
erpnext/accounts/doctype/customer_item/__init__.py
Normal file
31
erpnext/accounts/doctype/customer_item/customer_item.json
Normal file
31
erpnext/accounts/doctype/customer_item/customer_item.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-05 14:04:54.266353",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"customer"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "customer",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Customer ",
|
||||||
|
"options": "Customer"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-05-06 10:02:32.967841",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Customer Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
8
erpnext/accounts/doctype/customer_item/customer_item.py
Normal file
8
erpnext/accounts/doctype/customer_item/customer_item.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class CustomerItem(Document):
|
||||||
|
pass
|
@ -2,12 +2,13 @@
|
|||||||
"actions": [],
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"autoname": "field:title",
|
"autoname": "naming_series:",
|
||||||
"creation": "2014-02-21 15:02:51",
|
"creation": "2014-02-21 15:02:51",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"applicability_section",
|
"applicability_section",
|
||||||
|
"naming_series",
|
||||||
"title",
|
"title",
|
||||||
"disable",
|
"disable",
|
||||||
"apply_on",
|
"apply_on",
|
||||||
@ -95,8 +96,7 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Title",
|
"label": "Title",
|
||||||
"no_copy": 1,
|
"no_copy": 1,
|
||||||
"reqd": 1,
|
"reqd": 1
|
||||||
"unique": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
@ -571,6 +571,13 @@
|
|||||||
"fieldname": "is_recursive",
|
"fieldname": "is_recursive",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Is Recursive"
|
"label": "Is Recursive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default": "PRLE-.####",
|
||||||
|
"fieldname": "naming_series",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Naming Series",
|
||||||
|
"options": "PRLE-.####"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-gift",
|
"icon": "fa fa-gift",
|
||||||
@ -634,5 +641,6 @@
|
|||||||
],
|
],
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
}
|
"title_field": "title"
|
||||||
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -25,22 +25,31 @@ product_discount_fields = ['free_item', 'free_qty', 'free_item_uom',
|
|||||||
|
|
||||||
class PromotionalScheme(Document):
|
class PromotionalScheme(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
|
if not self.selling and not self.buying:
|
||||||
|
frappe.throw(_("Either 'Selling' or 'Buying' must be selected"), title=_("Mandatory"))
|
||||||
if not (self.price_discount_slabs
|
if not (self.price_discount_slabs
|
||||||
or self.product_discount_slabs):
|
or self.product_discount_slabs):
|
||||||
frappe.throw(_("Price or product discount slabs are required"))
|
frappe.throw(_("Price or product discount slabs are required"))
|
||||||
|
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
data = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"],
|
pricing_rules = frappe.get_all(
|
||||||
filters = {'promotional_scheme': self.name}) or {}
|
'Pricing Rule',
|
||||||
|
fields = ["promotional_scheme_id", "name", "creation"],
|
||||||
|
filters = {
|
||||||
|
'promotional_scheme': self.name,
|
||||||
|
'applicable_for': self.applicable_for
|
||||||
|
},
|
||||||
|
order_by = 'creation asc',
|
||||||
|
) or {}
|
||||||
|
self.update_pricing_rules(pricing_rules)
|
||||||
|
|
||||||
self.update_pricing_rules(data)
|
def update_pricing_rules(self, pricing_rules):
|
||||||
|
|
||||||
def update_pricing_rules(self, data):
|
|
||||||
rules = {}
|
rules = {}
|
||||||
count = 0
|
count = 0
|
||||||
|
names = []
|
||||||
for d in data:
|
for rule in pricing_rules:
|
||||||
rules[d.get('promotional_scheme_id')] = d.get('name')
|
names.append(rule.name)
|
||||||
|
rules[rule.get('promotional_scheme_id')] = names
|
||||||
|
|
||||||
docs = get_pricing_rules(self, rules)
|
docs = get_pricing_rules(self, rules)
|
||||||
|
|
||||||
@ -57,9 +66,9 @@ class PromotionalScheme(Document):
|
|||||||
frappe.msgprint(_("New {0} pricing rules are created").format(count))
|
frappe.msgprint(_("New {0} pricing rules are created").format(count))
|
||||||
|
|
||||||
def on_trash(self):
|
def on_trash(self):
|
||||||
for d in frappe.get_all('Pricing Rule',
|
for rule in frappe.get_all('Pricing Rule',
|
||||||
{'promotional_scheme': self.name}):
|
{'promotional_scheme': self.name}):
|
||||||
frappe.delete_doc('Pricing Rule', d.name)
|
frappe.delete_doc('Pricing Rule', rule.name)
|
||||||
|
|
||||||
def get_pricing_rules(doc, rules = {}):
|
def get_pricing_rules(doc, rules = {}):
|
||||||
new_doc = []
|
new_doc = []
|
||||||
@ -73,42 +82,80 @@ def get_pricing_rules(doc, rules = {}):
|
|||||||
def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}):
|
def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}):
|
||||||
new_doc = []
|
new_doc = []
|
||||||
args = get_args_for_pricing_rule(doc)
|
args = get_args_for_pricing_rule(doc)
|
||||||
for d in doc.get(child_doc):
|
applicable_for = frappe.scrub(doc.get('applicable_for'))
|
||||||
|
for idx, d in enumerate(doc.get(child_doc)):
|
||||||
if d.name in rules:
|
if d.name in rules:
|
||||||
pr = frappe.get_doc('Pricing Rule', rules.get(d.name))
|
for applicable_for_value in args.get(applicable_for):
|
||||||
|
temp_args = args.copy()
|
||||||
|
docname = frappe.get_all(
|
||||||
|
'Pricing Rule',
|
||||||
|
fields = ["promotional_scheme_id", "name", applicable_for],
|
||||||
|
filters = {
|
||||||
|
'promotional_scheme_id': d.name,
|
||||||
|
applicable_for: applicable_for_value
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if docname:
|
||||||
|
pr = frappe.get_doc('Pricing Rule', docname[0].get('name'))
|
||||||
|
temp_args[applicable_for] = applicable_for_value
|
||||||
|
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
|
||||||
|
else:
|
||||||
|
pr = frappe.new_doc("Pricing Rule")
|
||||||
|
pr.title = doc.name
|
||||||
|
temp_args[applicable_for] = applicable_for_value
|
||||||
|
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
|
||||||
|
|
||||||
|
new_doc.append(pr)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
pr = frappe.new_doc("Pricing Rule")
|
applicable_for_values = args.get(applicable_for) or []
|
||||||
pr.title = make_autoname("{0}/.####".format(doc.name))
|
for applicable_for_value in applicable_for_values:
|
||||||
|
pr = frappe.new_doc("Pricing Rule")
|
||||||
pr.update(args)
|
pr.title = doc.name
|
||||||
for field in (other_fields + discount_fields):
|
temp_args = args.copy()
|
||||||
pr.set(field, d.get(field))
|
temp_args[applicable_for] = applicable_for_value
|
||||||
|
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
|
||||||
pr.promotional_scheme_id = d.name
|
new_doc.append(pr)
|
||||||
pr.promotional_scheme = doc.name
|
|
||||||
pr.disable = d.disable if d.disable else doc.disable
|
|
||||||
pr.price_or_product_discount = ('Price'
|
|
||||||
if child_doc == 'price_discount_slabs' else 'Product')
|
|
||||||
|
|
||||||
for field in ['items', 'item_groups', 'brands']:
|
|
||||||
if doc.get(field):
|
|
||||||
pr.set(field, [])
|
|
||||||
|
|
||||||
apply_on = frappe.scrub(doc.get('apply_on'))
|
|
||||||
for d in doc.get(field):
|
|
||||||
pr.append(field, {
|
|
||||||
apply_on: d.get(apply_on),
|
|
||||||
'uom': d.uom
|
|
||||||
})
|
|
||||||
|
|
||||||
new_doc.append(pr)
|
|
||||||
|
|
||||||
return new_doc
|
return new_doc
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
|
||||||
|
pr.update(args)
|
||||||
|
for field in (other_fields + discount_fields):
|
||||||
|
pr.set(field, child_doc_fields.get(field))
|
||||||
|
|
||||||
|
pr.promotional_scheme_id = child_doc_fields.name
|
||||||
|
pr.promotional_scheme = doc.name
|
||||||
|
pr.disable = child_doc_fields.disable if child_doc_fields.disable else doc.disable
|
||||||
|
pr.price_or_product_discount = ('Price'
|
||||||
|
if child_doc == 'price_discount_slabs' else 'Product')
|
||||||
|
|
||||||
|
for field in ['items', 'item_groups', 'brands']:
|
||||||
|
if doc.get(field):
|
||||||
|
pr.set(field, [])
|
||||||
|
|
||||||
|
apply_on = frappe.scrub(doc.get('apply_on'))
|
||||||
|
for d in doc.get(field):
|
||||||
|
pr.append(field, {
|
||||||
|
apply_on: d.get(apply_on),
|
||||||
|
'uom': d.uom
|
||||||
|
})
|
||||||
|
return pr
|
||||||
|
|
||||||
def get_args_for_pricing_rule(doc):
|
def get_args_for_pricing_rule(doc):
|
||||||
args = { 'promotional_scheme': doc.name }
|
args = { 'promotional_scheme': doc.name }
|
||||||
|
applicable_for = frappe.scrub(doc.get('applicable_for'))
|
||||||
|
|
||||||
for d in pricing_rule_fields:
|
for d in pricing_rule_fields:
|
||||||
args[d] = doc.get(d)
|
if d == applicable_for:
|
||||||
|
items = []
|
||||||
|
for applicable_for_values in doc.get(applicable_for):
|
||||||
|
items.append(applicable_for_values.get(applicable_for))
|
||||||
|
args[d] = items
|
||||||
|
else:
|
||||||
|
args[d] = doc.get(d)
|
||||||
return args
|
return args
|
||||||
|
@ -7,4 +7,54 @@ import frappe
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
class TestPromotionalScheme(unittest.TestCase):
|
class TestPromotionalScheme(unittest.TestCase):
|
||||||
pass
|
def test_promotional_scheme(self):
|
||||||
|
ps = make_promotional_scheme()
|
||||||
|
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"],
|
||||||
|
filters = {'promotional_scheme': ps.name})
|
||||||
|
self.assertTrue(len(price_rules),1)
|
||||||
|
price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
|
||||||
|
self.assertTrue(price_doc_details.customer, '_Test Customer')
|
||||||
|
self.assertTrue(price_doc_details.min_qty, 4)
|
||||||
|
self.assertTrue(price_doc_details.discount_percentage, 20)
|
||||||
|
|
||||||
|
ps.price_discount_slabs[0].min_qty = 6
|
||||||
|
ps.append('customer', {
|
||||||
|
'customer': "_Test Customer 2"})
|
||||||
|
ps.save()
|
||||||
|
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"],
|
||||||
|
filters = {'promotional_scheme': ps.name})
|
||||||
|
self.assertTrue(len(price_rules), 2)
|
||||||
|
|
||||||
|
price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[1].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
|
||||||
|
self.assertTrue(price_doc_details.customer, '_Test Customer 2')
|
||||||
|
self.assertTrue(price_doc_details.min_qty, 6)
|
||||||
|
self.assertTrue(price_doc_details.discount_percentage, 20)
|
||||||
|
|
||||||
|
price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
|
||||||
|
self.assertTrue(price_doc_details.customer, '_Test Customer')
|
||||||
|
self.assertTrue(price_doc_details.min_qty, 6)
|
||||||
|
|
||||||
|
frappe.delete_doc('Promotional Scheme', ps.name)
|
||||||
|
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"],
|
||||||
|
filters = {'promotional_scheme': ps.name})
|
||||||
|
self.assertEqual(price_rules, [])
|
||||||
|
|
||||||
|
def make_promotional_scheme():
|
||||||
|
ps = frappe.new_doc('Promotional Scheme')
|
||||||
|
ps.name = '_Test Scheme'
|
||||||
|
ps.append('items',{
|
||||||
|
'item_code': '_Test Item'
|
||||||
|
})
|
||||||
|
ps.selling = 1
|
||||||
|
ps.append('price_discount_slabs',{
|
||||||
|
'min_qty': 4,
|
||||||
|
'discount_percentage': 20,
|
||||||
|
'rule_description': 'Test'
|
||||||
|
})
|
||||||
|
ps.applicable_for = 'Customer'
|
||||||
|
ps.append('customer',{
|
||||||
|
'customer': "_Test Customer"
|
||||||
|
})
|
||||||
|
ps.save()
|
||||||
|
|
||||||
|
return ps
|
@ -275,7 +275,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
// Do not update if inter company reference is there as the details will already be updated
|
// Do not update if inter company reference is there as the details will already be updated
|
||||||
if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference)
|
if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
|
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
|
||||||
{
|
{
|
||||||
posting_date: this.frm.doc.posting_date,
|
posting_date: this.frm.doc.posting_date,
|
||||||
|
@ -522,7 +522,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
and flt(d.base_tax_amount_after_discount_amount)]
|
and flt(d.base_tax_amount_after_discount_amount)]
|
||||||
|
|
||||||
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
|
exchange_rate_map, net_rate_map = get_purchase_document_details(self)
|
||||||
|
|
||||||
|
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if flt(item.base_net_amount):
|
if flt(item.base_net_amount):
|
||||||
account_currency = get_account_currency(item.expense_account)
|
account_currency = get_account_currency(item.expense_account)
|
||||||
@ -613,7 +615,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account)
|
if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account)
|
||||||
|
|
||||||
if not item.is_fixed_asset:
|
if not item.is_fixed_asset:
|
||||||
dummy, amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting)
|
dummy, amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||||
else:
|
else:
|
||||||
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
|
||||||
|
|
||||||
@ -855,9 +857,10 @@ class PurchaseInvoice(BuyingController):
|
|||||||
def make_tax_gl_entries(self, gl_entries):
|
def make_tax_gl_entries(self, gl_entries):
|
||||||
# tax table gl entries
|
# tax table gl entries
|
||||||
valuation_tax = {}
|
valuation_tax = {}
|
||||||
|
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||||
|
|
||||||
for tax in self.get("taxes"):
|
for tax in self.get("taxes"):
|
||||||
amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting)
|
amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting)
|
||||||
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
|
||||||
account_currency = get_account_currency(tax.account_head)
|
account_currency = get_account_currency(tax.account_head)
|
||||||
|
|
||||||
|
@ -271,7 +271,7 @@ class TestPurchaseInvoice(unittest.TestCase):
|
|||||||
enable_discount_accounting()
|
enable_discount_accounting()
|
||||||
additional_discount_account = create_account(account_name="Discount Account",
|
additional_discount_account = create_account(account_name="Discount Account",
|
||||||
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
||||||
|
|
||||||
pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC")
|
pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC")
|
||||||
pi.apply_discount_on = "Grand Total"
|
pi.apply_discount_on = "Grand Total"
|
||||||
pi.additional_discount_account = additional_discount_account
|
pi.additional_discount_account = additional_discount_account
|
||||||
|
@ -862,7 +862,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-12 20:14:45.506639",
|
"modified": "2021-08-12 20:14:48.506639",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Invoice Item",
|
"name": "Purchase Invoice Item",
|
||||||
|
@ -447,6 +447,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
this.frm.refresh_field("outstanding_amount");
|
this.frm.refresh_field("outstanding_amount");
|
||||||
this.frm.refresh_field("paid_amount");
|
this.frm.refresh_field("paid_amount");
|
||||||
this.frm.refresh_field("base_paid_amount");
|
this.frm.refresh_field("base_paid_amount");
|
||||||
|
},
|
||||||
|
|
||||||
|
currency() {
|
||||||
|
this._super();
|
||||||
|
$.each(cur_frm.doc.timesheets, function(i, d) {
|
||||||
|
let row = frappe.get_doc(d.doctype, d.name)
|
||||||
|
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
|
||||||
|
});
|
||||||
|
calculate_total_billing_amount(cur_frm)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -846,7 +855,8 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
'time_sheet': row.parent,
|
'time_sheet': row.parent,
|
||||||
'billing_hours': row.billing_hours,
|
'billing_hours': row.billing_hours,
|
||||||
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
|
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
|
||||||
'timesheet_detail': row.name
|
'timesheet_detail': row.name,
|
||||||
|
'project_name': row.project_name
|
||||||
});
|
});
|
||||||
frm.refresh_field('timesheets');
|
frm.refresh_field('timesheets');
|
||||||
calculate_total_billing_amount(frm);
|
calculate_total_billing_amount(frm);
|
||||||
@ -965,43 +975,34 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
frappe.ui.form.on('Sales Invoice Timesheet', {
|
|
||||||
time_sheet: function(frm, cdt, cdn){
|
|
||||||
var d = locals[cdt][cdn];
|
|
||||||
if(d.time_sheet) {
|
|
||||||
frappe.call({
|
|
||||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_data",
|
|
||||||
args: {
|
|
||||||
'name': d.time_sheet,
|
|
||||||
'project': frm.doc.project || null
|
|
||||||
},
|
|
||||||
callback: function(r, rt) {
|
|
||||||
if(r.message){
|
|
||||||
let data = r.message;
|
|
||||||
frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours);
|
|
||||||
frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount);
|
|
||||||
frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail);
|
|
||||||
calculate_total_billing_amount(frm)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
var calculate_total_billing_amount = function(frm) {
|
var calculate_total_billing_amount = function(frm) {
|
||||||
var doc = frm.doc;
|
var doc = frm.doc;
|
||||||
|
|
||||||
doc.total_billing_amount = 0.0
|
doc.total_billing_amount = 0.0
|
||||||
if(doc.timesheets) {
|
if (doc.timesheets) {
|
||||||
$.each(doc.timesheets, function(index, data){
|
$.each(doc.timesheets, function(index, data){
|
||||||
doc.total_billing_amount += data.billing_amount
|
doc.total_billing_amount += flt(data.billing_amount)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh_field('total_billing_amount')
|
refresh_field('total_billing_amount')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var set_timesheet_detail_rate = function(cdt, cdn, currency, timelog) {
|
||||||
|
frappe.call({
|
||||||
|
method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate",
|
||||||
|
args: {
|
||||||
|
timelog: timelog,
|
||||||
|
currency: currency
|
||||||
|
},
|
||||||
|
callback: function(r) {
|
||||||
|
if (!r.exc && r.message) {
|
||||||
|
frappe.model.set_value(cdt, cdn, 'billing_amount', r.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
var select_loyalty_program = function(frm, loyalty_programs) {
|
var select_loyalty_program = function(frm, loyalty_programs) {
|
||||||
var dialog = new frappe.ui.Dialog({
|
var dialog = new frappe.ui.Dialog({
|
||||||
title: __("Select Loyalty Program"),
|
title: __("Select Loyalty Program"),
|
||||||
|
@ -2331,7 +2331,9 @@
|
|||||||
"depends_on": "grand_total",
|
"depends_on": "grand_total",
|
||||||
"fieldname": "disable_rounded_total",
|
"fieldname": "disable_rounded_total",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Disable Rounded Total"
|
"label": "Disable Rounded Total",
|
||||||
|
"show_days": 1,
|
||||||
|
"show_seconds": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "additional_discount_account",
|
"fieldname": "additional_discount_account",
|
||||||
@ -2375,7 +2377,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2021-08-06 23:02:20.445127",
|
"modified": "2021-08-07 23:02:20.445127",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
@ -888,8 +888,10 @@ class SalesInvoice(SellingController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def make_tax_gl_entries(self, gl_entries):
|
def make_tax_gl_entries(self, gl_entries):
|
||||||
|
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||||
|
|
||||||
for tax in self.get("taxes"):
|
for tax in self.get("taxes"):
|
||||||
amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting)
|
amount, base_amount = self.get_tax_amounts(tax, enable_discount_accounting)
|
||||||
|
|
||||||
if flt(tax.base_tax_amount_after_discount_amount):
|
if flt(tax.base_tax_amount_after_discount_amount):
|
||||||
account_currency = get_account_currency(tax.account_head)
|
account_currency = get_account_currency(tax.account_head)
|
||||||
@ -920,6 +922,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
def make_item_gl_entries(self, gl_entries):
|
def make_item_gl_entries(self, gl_entries):
|
||||||
# income account gl entries
|
# income account gl entries
|
||||||
|
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
|
||||||
|
|
||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
if flt(item.base_net_amount, item.precision("base_net_amount")):
|
||||||
@ -933,7 +936,7 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
if asset.calculate_depreciation:
|
if asset.calculate_depreciation:
|
||||||
self.reset_depreciation_schedule(asset)
|
self.reset_depreciation_schedule(asset)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
|
fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset,
|
||||||
item.base_net_amount, item.finance_book)
|
item.base_net_amount, item.finance_book)
|
||||||
@ -954,7 +957,7 @@ class SalesInvoice(SellingController):
|
|||||||
income_account = (item.income_account
|
income_account = (item.income_account
|
||||||
if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
|
if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
|
||||||
|
|
||||||
amount, base_amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting)
|
amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
|
||||||
|
|
||||||
account_currency = get_account_currency(income_account)
|
account_currency = get_account_currency(income_account)
|
||||||
gl_entries.append(
|
gl_entries.append(
|
||||||
@ -980,7 +983,7 @@ class SalesInvoice(SellingController):
|
|||||||
asset = frappe.get_doc("Asset", item.asset)
|
asset = frappe.get_doc("Asset", item.asset)
|
||||||
else:
|
else:
|
||||||
frappe.throw(_(
|
frappe.throw(_(
|
||||||
"Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
|
"Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name),
|
||||||
title=_("Missing Asset")
|
title=_("Missing Asset")
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -997,7 +1000,7 @@ class SalesInvoice(SellingController):
|
|||||||
asset.flags.ignore_validate_update_after_submit = True
|
asset.flags.ignore_validate_update_after_submit = True
|
||||||
asset.prepare_depreciation_data(self.posting_date)
|
asset.prepare_depreciation_data(self.posting_date)
|
||||||
asset.save()
|
asset.save()
|
||||||
|
|
||||||
post_depreciation_entries(self.posting_date)
|
post_depreciation_entries(self.posting_date)
|
||||||
|
|
||||||
def reset_depreciation_schedule(self, asset):
|
def reset_depreciation_schedule(self, asset):
|
||||||
@ -1037,7 +1040,7 @@ class SalesInvoice(SellingController):
|
|||||||
finance_book = schedule.finance_book
|
finance_book = schedule.finance_book
|
||||||
else:
|
else:
|
||||||
row += 1
|
row += 1
|
||||||
|
|
||||||
if schedule.schedule_date == posting_date_of_original_invoice:
|
if schedule.schedule_date == posting_date_of_original_invoice:
|
||||||
if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice):
|
if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice):
|
||||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||||
@ -1047,17 +1050,17 @@ class SalesInvoice(SellingController):
|
|||||||
def get_posting_date_of_sales_invoice(self):
|
def get_posting_date_of_sales_invoice(self):
|
||||||
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
|
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
|
||||||
|
|
||||||
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
||||||
def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice):
|
def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice):
|
||||||
for finance_book in asset.get('finance_books'):
|
for finance_book in asset.get('finance_books'):
|
||||||
if schedule.finance_book == finance_book.finance_book:
|
if schedule.finance_book == finance_book.finance_book:
|
||||||
orginal_schedule_date = add_months(finance_book.depreciation_start_date,
|
orginal_schedule_date = add_months(finance_book.depreciation_start_date,
|
||||||
row * cint(finance_book.frequency_of_depreciation))
|
row * cint(finance_book.frequency_of_depreciation))
|
||||||
|
|
||||||
if orginal_schedule_date == posting_date_of_original_invoice:
|
if orginal_schedule_date == posting_date_of_original_invoice:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enable_discount_accounting(self):
|
def enable_discount_accounting(self):
|
||||||
if not hasattr(self, "_enable_discount_accounting"):
|
if not hasattr(self, "_enable_discount_accounting"):
|
||||||
|
@ -2110,7 +2110,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
discount_account = create_account(account_name="Discount Account",
|
discount_account = create_account(account_name="Discount Account",
|
||||||
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
||||||
si = create_sales_invoice(discount_account=discount_account, discount_percentage=10, rate=90)
|
si = create_sales_invoice(discount_account=discount_account, discount_percentage=10, rate=90)
|
||||||
|
|
||||||
expected_gle = [
|
expected_gle = [
|
||||||
["Debtors - _TC", 90.0, 0.0, nowdate()],
|
["Debtors - _TC", 90.0, 0.0, nowdate()],
|
||||||
["Discount Account - _TC", 10.0, 0.0, nowdate()],
|
["Discount Account - _TC", 10.0, 0.0, nowdate()],
|
||||||
@ -2126,7 +2126,7 @@ class TestSalesInvoice(unittest.TestCase):
|
|||||||
enable_discount_accounting()
|
enable_discount_accounting()
|
||||||
additional_discount_account = create_account(account_name="Discount Account",
|
additional_discount_account = create_account(account_name="Discount Account",
|
||||||
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
parent_account="Indirect Expenses - _TC", company="_Test Company")
|
||||||
|
|
||||||
si = create_sales_invoice(parent_cost_center='Main - _TC', do_not_save=1)
|
si = create_sales_invoice(parent_cost_center='Main - _TC', do_not_save=1)
|
||||||
si.apply_discount_on = "Grand Total"
|
si.apply_discount_on = "Grand Total"
|
||||||
si.additional_discount_account = additional_discount_account
|
si.additional_discount_account = additional_discount_account
|
||||||
|
@ -833,7 +833,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-12 20:15:42.668399",
|
"modified": "2021-08-12 20:15:47.668399",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Item",
|
"name": "Sales Invoice Item",
|
||||||
|
@ -9,7 +9,9 @@
|
|||||||
"description",
|
"description",
|
||||||
"billing_hours",
|
"billing_hours",
|
||||||
"billing_amount",
|
"billing_amount",
|
||||||
|
"column_break_5",
|
||||||
"time_sheet",
|
"time_sheet",
|
||||||
|
"project_name",
|
||||||
"timesheet_detail"
|
"timesheet_detail"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
@ -61,11 +63,21 @@
|
|||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Description",
|
"label": "Description",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_5",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "project_name",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Project Name",
|
||||||
|
"read_only": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-20 22:33:57.234846",
|
"modified": "2021-06-08 14:43:02.748981",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice Timesheet",
|
"name": "Sales Invoice Timesheet",
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-06 16:17:44.329943",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"sales_partner"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "sales_partner",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Sales Partner ",
|
||||||
|
"options": "Sales Partner"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-05-07 10:43:37.532095",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Sales Partner Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class SalesPartnerItem(Document):
|
||||||
|
pass
|
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-06 16:19:22.040795",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"supplier_group"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "supplier_group",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Supplier Group",
|
||||||
|
"options": "Supplier Group"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-05-07 10:43:59.877938",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Supplier Group Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class SupplierGroupItem(Document):
|
||||||
|
pass
|
0
erpnext/accounts/doctype/supplier_item/__init__.py
Normal file
0
erpnext/accounts/doctype/supplier_item/__init__.py
Normal file
31
erpnext/accounts/doctype/supplier_item/supplier_item.json
Normal file
31
erpnext/accounts/doctype/supplier_item/supplier_item.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-06 16:18:54.758468",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"supplier"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "supplier",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Supplier",
|
||||||
|
"options": "Supplier"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-05-07 10:44:09.707778",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Supplier Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
8
erpnext/accounts/doctype/supplier_item/supplier_item.py
Normal file
8
erpnext/accounts/doctype/supplier_item/supplier_item.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class SupplierItem(Document):
|
||||||
|
pass
|
0
erpnext/accounts/doctype/territory_item/__init__.py
Normal file
0
erpnext/accounts/doctype/territory_item/__init__.py
Normal file
31
erpnext/accounts/doctype/territory_item/territory_item.json
Normal file
31
erpnext/accounts/doctype/territory_item/territory_item.json
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2021-05-06 16:16:51.885441",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"territory"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "territory",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Territory",
|
||||||
|
"options": "Territory"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2021-05-07 10:43:26.641030",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Territory Item",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,8 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class TerritoryItem(Document):
|
||||||
|
pass
|
@ -176,16 +176,16 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
for d in self.get('finance_books'):
|
for d in self.get('finance_books'):
|
||||||
self.validate_asset_finance_books(d)
|
self.validate_asset_finance_books(d)
|
||||||
|
|
||||||
start = self.clear_depreciation_schedule()
|
start = self.clear_depreciation_schedule()
|
||||||
|
|
||||||
# value_after_depreciation - current Asset value
|
# value_after_depreciation - current Asset value
|
||||||
if d.value_after_depreciation:
|
if d.value_after_depreciation:
|
||||||
value_after_depreciation = (flt(d.value_after_depreciation) -
|
value_after_depreciation = (flt(d.value_after_depreciation) -
|
||||||
flt(self.opening_accumulated_depreciation))
|
flt(self.opening_accumulated_depreciation))
|
||||||
else:
|
else:
|
||||||
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||||
flt(self.opening_accumulated_depreciation))
|
flt(self.opening_accumulated_depreciation))
|
||||||
|
|
||||||
d.value_after_depreciation = value_after_depreciation
|
d.value_after_depreciation = value_after_depreciation
|
||||||
|
|
||||||
@ -321,10 +321,10 @@ class Asset(AccountsController):
|
|||||||
def get_from_date(self, finance_book):
|
def get_from_date(self, finance_book):
|
||||||
if not self.get('schedules'):
|
if not self.get('schedules'):
|
||||||
return self.available_for_use_date
|
return self.available_for_use_date
|
||||||
|
|
||||||
if len(self.finance_books) == 1:
|
if len(self.finance_books) == 1:
|
||||||
return self.schedules[-1].schedule_date
|
return self.schedules[-1].schedule_date
|
||||||
|
|
||||||
from_date = ""
|
from_date = ""
|
||||||
for schedule in self.get('schedules'):
|
for schedule in self.get('schedules'):
|
||||||
if schedule.finance_book == finance_book:
|
if schedule.finance_book == finance_book:
|
||||||
@ -831,4 +831,4 @@ def get_depreciation_amount(asset, depreciable_value, row):
|
|||||||
else:
|
else:
|
||||||
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
|
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
|
||||||
|
|
||||||
return depreciation_amount
|
return depreciation_amount
|
||||||
|
46
erpnext/change_log/v13/v13_9_0.md
Normal file
46
erpnext/change_log/v13/v13_9_0.md
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# Version 13.9.0 Release Notes
|
||||||
|
|
||||||
|
### Features & Enhancements
|
||||||
|
- Organizational Chart ([#26261](https://github.com/frappe/erpnext/pull/26261))
|
||||||
|
- Enable discount accounting ([#26579](https://github.com/frappe/erpnext/pull/26579))
|
||||||
|
- Added multi-select fields in promotional scheme to create multiple pricing rules ([#25622](https://github.com/frappe/erpnext/pull/25622))
|
||||||
|
- Over transfer allowance for material transfers ([#26814](https://github.com/frappe/erpnext/pull/26814))
|
||||||
|
- Enhancements in Tax Withholding Category ([#26661](https://github.com/frappe/erpnext/pull/26661))
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- Sales Return cancellation if linked with Payment Entry ([#26883](https://github.com/frappe/erpnext/pull/26883))
|
||||||
|
- Production plan not fetching sales order of a variant ([#25845](https://github.com/frappe/erpnext/pull/25845))
|
||||||
|
- Stock Analytics Report must consider warehouse during calculation ([#26908](https://github.com/frappe/erpnext/pull/26908))
|
||||||
|
- Incorrect date difference calculation ([#26805](https://github.com/frappe/erpnext/pull/26805))
|
||||||
|
- Tax calculation for Recurring additional salary ([#24206](https://github.com/frappe/erpnext/pull/24206))
|
||||||
|
- Cannot cancel payment entry if linked with invoices ([#26703](https://github.com/frappe/erpnext/pull/26703))
|
||||||
|
- Included company in link document type filters for contact ([#26576](https://github.com/frappe/erpnext/pull/26576))
|
||||||
|
- Fetch Payment Terms from linked Sales/Purchase Order ([#26723](https://github.com/frappe/erpnext/pull/26723))
|
||||||
|
- Let all System Managers be able to delete Company transactions ([#26819](https://github.com/frappe/erpnext/pull/26819))
|
||||||
|
- Bank remittance report issue ([#26398](https://github.com/frappe/erpnext/pull/26398))
|
||||||
|
- Faulty Gl Entry for Asset LCVs ([#26803](https://github.com/frappe/erpnext/pull/26803))
|
||||||
|
- Clean Serial No input on Server Side ([#26878](https://github.com/frappe/erpnext/pull/26878))
|
||||||
|
- Supplier invoice importer fix v13 ([#26633](https://github.com/frappe/erpnext/pull/26633))
|
||||||
|
- POS payment modes displayed wrong total ([#26808](https://github.com/frappe/erpnext/pull/26808))
|
||||||
|
- Fetching of item tax from hsn code ([#26736](https://github.com/frappe/erpnext/pull/26736))
|
||||||
|
- Cannot cancel invoice if IRN cancelled on portal ([#26879](https://github.com/frappe/erpnext/pull/26879))
|
||||||
|
- Validate python expressions ([#26856](https://github.com/frappe/erpnext/pull/26856))
|
||||||
|
- POS Item Cart non-stop scroll issue ([#26693](https://github.com/frappe/erpnext/pull/26693))
|
||||||
|
- Add mandatory depends on condition for export type field ([#26958](https://github.com/frappe/erpnext/pull/26958))
|
||||||
|
- Cannot generate IRNs for standalone credit notes ([#26824](https://github.com/frappe/erpnext/pull/26824))
|
||||||
|
- Added progress bar in Repost Item Valuation to check the status of reposting ([#26630](https://github.com/frappe/erpnext/pull/26630))
|
||||||
|
- TDS calculation for first threshold breach for TDS category 194Q ([#26710](https://github.com/frappe/erpnext/pull/26710))
|
||||||
|
- Student category mapping from the program enrollment tool ([#26739](https://github.com/frappe/erpnext/pull/26739))
|
||||||
|
- Cost center & account validation in Sales/Purchase Taxes and Charges ([#26881](https://github.com/frappe/erpnext/pull/26881))
|
||||||
|
- Reset weight_per_unit on replacing Item ([#26791](https://github.com/frappe/erpnext/pull/26791))
|
||||||
|
- Do not fetch fully return issued purchase receipts ([#26825](https://github.com/frappe/erpnext/pull/26825))
|
||||||
|
- Incorrect amount in work order required items table. ([#26585](https://github.com/frappe/erpnext/pull/26585))
|
||||||
|
- Additional discount calculations in Invoices ([#26553](https://github.com/frappe/erpnext/pull/26553))
|
||||||
|
- Refactored Asset Repair ([#26415](https://github.com/frappe/erpnext/pull/25798))
|
||||||
|
- Exchange rate revaluation posting date and precision fixes ([#26650](https://github.com/frappe/erpnext/pull/26650))
|
||||||
|
- POS Invoice consolidated Sales Invoice field set to no copy ([#26768](https://github.com/frappe/erpnext/pull/26768))
|
||||||
|
- Consider grand total for threshold check ([#26683](https://github.com/frappe/erpnext/pull/26683))
|
||||||
|
- Budget variance missing values ([#26966](https://github.com/frappe/erpnext/pull/26966))
|
||||||
|
- GL Entries for exchange gain loss ([#26728](https://github.com/frappe/erpnext/pull/26728))
|
||||||
|
- Add missing cess amount in GSTR-3B report ([#26544](https://github.com/frappe/erpnext/pull/26544))
|
||||||
|
- GST Reports timeout issue ([#26575](https://github.com/frappe/erpnext/pull/26575))
|
@ -109,6 +109,15 @@ class ProductionPlan(Document):
|
|||||||
so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)]
|
so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)]
|
||||||
return so_mr_list
|
return so_mr_list
|
||||||
|
|
||||||
|
def get_bom_item(self):
|
||||||
|
"""Check if Item or if its Template has a BOM."""
|
||||||
|
bom_item = None
|
||||||
|
has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1})
|
||||||
|
if not has_bom:
|
||||||
|
template_item = frappe.db.get_value('Item', self.item_code, ['variant_of'])
|
||||||
|
bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
|
||||||
|
return bom_item
|
||||||
|
|
||||||
def get_so_items(self):
|
def get_so_items(self):
|
||||||
# Check for empty table or empty rows
|
# Check for empty table or empty rows
|
||||||
if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"):
|
if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"):
|
||||||
@ -117,16 +126,26 @@ class ProductionPlan(Document):
|
|||||||
so_list = self.get_so_mr_list("sales_order", "sales_orders")
|
so_list = self.get_so_mr_list("sales_order", "sales_orders")
|
||||||
|
|
||||||
item_condition = ""
|
item_condition = ""
|
||||||
if self.item_code:
|
bom_item = "bom.item = so_item.item_code"
|
||||||
|
if self.item_code and frappe.db.exists('Item', self.item_code):
|
||||||
|
bom_item = self.get_bom_item() or bom_item
|
||||||
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
|
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
|
||||||
|
|
||||||
items = frappe.db.sql("""select distinct parent, item_code, warehouse,
|
items = frappe.db.sql("""
|
||||||
(qty - work_order_qty) * conversion_factor as pending_qty, description, name
|
select
|
||||||
from `tabSales Order Item` so_item
|
distinct parent, item_code, warehouse,
|
||||||
where parent in (%s) and docstatus = 1 and qty > work_order_qty
|
(qty - work_order_qty) * conversion_factor as pending_qty,
|
||||||
and exists (select name from `tabBOM` bom where bom.item=so_item.item_code
|
description, name
|
||||||
and bom.is_active = 1) %s""" % \
|
from
|
||||||
(", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1)
|
`tabSales Order Item` so_item
|
||||||
|
where
|
||||||
|
parent in (%s) and docstatus = 1 and qty > work_order_qty
|
||||||
|
and exists (select name from `tabBOM` bom where %s
|
||||||
|
and bom.is_active = 1) %s""" %
|
||||||
|
(", ".join(["%s"] * len(so_list)),
|
||||||
|
bom_item,
|
||||||
|
item_condition),
|
||||||
|
tuple(so_list), as_dict=1)
|
||||||
|
|
||||||
if self.item_code:
|
if self.item_code:
|
||||||
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
|
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
|
||||||
@ -683,6 +702,7 @@ def get_material_request_items(row, sales_order, company,
|
|||||||
|
|
||||||
def get_sales_orders(self):
|
def get_sales_orders(self):
|
||||||
so_filter = item_filter = ""
|
so_filter = item_filter = ""
|
||||||
|
bom_item = "bom.item = so_item.item_code"
|
||||||
if self.from_date:
|
if self.from_date:
|
||||||
so_filter += " and so.transaction_date >= %(from_date)s"
|
so_filter += " and so.transaction_date >= %(from_date)s"
|
||||||
if self.to_date:
|
if self.to_date:
|
||||||
@ -694,7 +714,8 @@ def get_sales_orders(self):
|
|||||||
if self.sales_order_status:
|
if self.sales_order_status:
|
||||||
so_filter += "and so.status = %(sales_order_status)s"
|
so_filter += "and so.status = %(sales_order_status)s"
|
||||||
|
|
||||||
if self.item_code:
|
if self.item_code and frappe.db.exists('Item', self.item_code):
|
||||||
|
bom_item = self.get_bom_item() or bom_item
|
||||||
item_filter += " and so_item.item_code = %(item)s"
|
item_filter += " and so_item.item_code = %(item)s"
|
||||||
|
|
||||||
open_so = frappe.db.sql("""
|
open_so = frappe.db.sql("""
|
||||||
@ -704,13 +725,13 @@ def get_sales_orders(self):
|
|||||||
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
|
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
|
||||||
and so.company = %(company)s
|
and so.company = %(company)s
|
||||||
and so_item.qty > so_item.work_order_qty {0} {1}
|
and so_item.qty > so_item.work_order_qty {0} {1}
|
||||||
and (exists (select name from `tabBOM` bom where bom.item=so_item.item_code
|
and (exists (select name from `tabBOM` bom where {2}
|
||||||
and bom.is_active = 1)
|
and bom.is_active = 1)
|
||||||
or exists (select name from `tabPacked Item` pi
|
or exists (select name from `tabPacked Item` pi
|
||||||
where pi.parent = so.name and pi.parent_item = so_item.item_code
|
where pi.parent = so.name and pi.parent_item = so_item.item_code
|
||||||
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
|
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
|
||||||
and bom.is_active = 1)))
|
and bom.is_active = 1)))
|
||||||
""".format(so_filter, item_filter), {
|
""".format(so_filter, item_filter, bom_item), {
|
||||||
"from_date": self.from_date,
|
"from_date": self.from_date,
|
||||||
"to_date": self.to_date,
|
"to_date": self.to_date,
|
||||||
"customer": self.customer,
|
"customer": self.customer,
|
||||||
|
@ -11,6 +11,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import get_sa
|
|||||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
|
from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
|
||||||
|
from erpnext.controllers.item_variant import create_variant
|
||||||
|
|
||||||
class TestProductionPlan(unittest.TestCase):
|
class TestProductionPlan(unittest.TestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
@ -271,6 +272,60 @@ class TestProductionPlan(unittest.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(warehouses, expected_warehouses)
|
self.assertEqual(warehouses, expected_warehouses)
|
||||||
|
|
||||||
|
def test_get_sales_order_with_variant(self):
|
||||||
|
if not frappe.db.exists('Item', {"item_code": 'PIV'}):
|
||||||
|
item = create_item('PIV', valuation_rate = 100)
|
||||||
|
variant_settings = {
|
||||||
|
"attributes": [
|
||||||
|
{
|
||||||
|
"attribute": "Colour"
|
||||||
|
},
|
||||||
|
],
|
||||||
|
"has_variants": 1
|
||||||
|
}
|
||||||
|
item.update(variant_settings)
|
||||||
|
item.save()
|
||||||
|
parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
|
||||||
|
if not frappe.db.exists('BOM', {"item": 'PIV'}):
|
||||||
|
parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
|
||||||
|
else:
|
||||||
|
parent_bom = frappe.get_doc('BOM', {"item": 'PIV'})
|
||||||
|
|
||||||
|
if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}):
|
||||||
|
variant = create_variant("PIV", {"Colour": "Red"})
|
||||||
|
variant.save()
|
||||||
|
variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
|
||||||
|
else:
|
||||||
|
variant = frappe.get_doc('Item', 'PIV-RED')
|
||||||
|
if not frappe.db.exists('BOM', {"item": 'PIV-RED'}):
|
||||||
|
variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
|
||||||
|
|
||||||
|
"""Testing when item variant has a BOM"""
|
||||||
|
so = make_sales_order(item_code="PIV-RED", qty=5)
|
||||||
|
pln = frappe.new_doc('Production Plan')
|
||||||
|
pln.company = so.company
|
||||||
|
pln.get_items_from = 'Sales Order'
|
||||||
|
pln.item_code = 'PIV-RED'
|
||||||
|
pln.get_open_sales_orders()
|
||||||
|
self.assertEqual(pln.sales_orders[0].sales_order, so.name)
|
||||||
|
pln.get_so_items()
|
||||||
|
self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
|
||||||
|
self.assertEqual(pln.po_items[0].bom_no, variant_bom.name)
|
||||||
|
so.cancel()
|
||||||
|
frappe.delete_doc('Sales Order', so.name)
|
||||||
|
variant_bom.cancel()
|
||||||
|
frappe.delete_doc('BOM', variant_bom.name)
|
||||||
|
|
||||||
|
"""Testing when item variant doesn't have a BOM"""
|
||||||
|
so = make_sales_order(item_code="PIV-RED", qty=5)
|
||||||
|
pln.get_open_sales_orders()
|
||||||
|
self.assertEqual(pln.sales_orders[0].sales_order, so.name)
|
||||||
|
pln.po_items = []
|
||||||
|
pln.get_so_items()
|
||||||
|
self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
|
||||||
|
self.assertEqual(pln.po_items[0].bom_no, parent_bom.name)
|
||||||
|
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def create_production_plan(**args):
|
def create_production_plan(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
@ -299,4 +299,5 @@ erpnext.patches.v13_0.delete_orphaned_tables
|
|||||||
erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16
|
erpnext.patches.v13_0.update_export_type_for_gst #2021-08-16
|
||||||
erpnext.patches.v13_0.update_tds_check_field #3
|
erpnext.patches.v13_0.update_tds_check_field #3
|
||||||
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
|
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
|
||||||
|
erpnext.patches.v13_0.update_recipient_email_digest
|
||||||
erpnext.patches.v13_0.shopify_deprecation_warning
|
erpnext.patches.v13_0.shopify_deprecation_warning
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
import click
|
import click
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
def execute():
|
def execute():
|
||||||
|
|
||||||
|
frappe.reload_doc("erpnext_integrations", "doctype", "shopify_settings")
|
||||||
|
if not frappe.db.get_single_value("Shopify Settings", "enable_shopify"):
|
||||||
|
return
|
||||||
|
|
||||||
click.secho(
|
click.secho(
|
||||||
"Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
|
"Shopify Integration is moved to a separate app and will be removed from ERPNext in version-14.\n"
|
||||||
"Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations",
|
"Please install the app to continue using the integration: https://github.com/frappe/ecommerce_integrations",
|
||||||
|
21
erpnext/patches/v13_0/update_recipient_email_digest.py
Normal file
21
erpnext/patches/v13_0/update_recipient_email_digest.py
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# Copyright (c) 2020, Frappe and Contributors
|
||||||
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
frappe.reload_doc("setup", "doctype", "Email Digest")
|
||||||
|
frappe.reload_doc("setup", "doctype", "Email Digest Recipient")
|
||||||
|
email_digests = frappe.db.get_list('Email Digest', fields=['name', 'recipient_list'])
|
||||||
|
for email_digest in email_digests:
|
||||||
|
if email_digest.recipient_list:
|
||||||
|
for recipient in email_digest.recipient_list.split("\n"):
|
||||||
|
doc = frappe.get_doc({
|
||||||
|
'doctype': 'Email Digest Recipient',
|
||||||
|
'parenttype': 'Email Digest',
|
||||||
|
'parentfield': 'recipients',
|
||||||
|
'parent': email_digest.name,
|
||||||
|
'recipient': recipient
|
||||||
|
})
|
||||||
|
doc.insert()
|
@ -112,11 +112,11 @@ class AdditionalSalary(Document):
|
|||||||
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
|
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
|
||||||
return amount_per_day * no_of_days
|
return amount_per_day * no_of_days
|
||||||
|
|
||||||
@frappe.whitelist()
|
|
||||||
def get_additional_salaries(employee, start_date, end_date, component_type):
|
def get_additional_salaries(employee, start_date, end_date, component_type):
|
||||||
additional_salary_list = frappe.db.sql("""
|
additional_salary_list = frappe.db.sql("""
|
||||||
select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite,
|
select name, salary_component as component, type, amount,
|
||||||
deduct_full_tax_on_selected_payroll_date, is_recurring
|
overwrite_salary_structure_amount as overwrite,
|
||||||
|
deduct_full_tax_on_selected_payroll_date
|
||||||
from `tabAdditional Salary`
|
from `tabAdditional Salary`
|
||||||
where employee=%(employee)s
|
where employee=%(employee)s
|
||||||
and docstatus = 1
|
and docstatus = 1
|
||||||
|
@ -12,7 +12,6 @@
|
|||||||
"year_to_date",
|
"year_to_date",
|
||||||
"section_break_5",
|
"section_break_5",
|
||||||
"additional_salary",
|
"additional_salary",
|
||||||
"is_recurring_additional_salary",
|
|
||||||
"statistical_component",
|
"statistical_component",
|
||||||
"depends_on_payment_days",
|
"depends_on_payment_days",
|
||||||
"exempted_from_income_tax",
|
"exempted_from_income_tax",
|
||||||
@ -236,19 +235,11 @@
|
|||||||
"label": "Year To Date",
|
"label": "Year To Date",
|
||||||
"options": "currency",
|
"options": "currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
|
||||||
{
|
|
||||||
"default": "0",
|
|
||||||
"depends_on": "eval:doc.parenttype=='Salary Slip' && doc.parentfield=='earnings' && doc.additional_salary",
|
|
||||||
"fieldname": "is_recurring_additional_salary",
|
|
||||||
"fieldtype": "Check",
|
|
||||||
"label": "Is Recurring Additional Salary",
|
|
||||||
"read_only": 1
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-03-14 13:39:15.847158",
|
"modified": "2021-01-14 13:39:15.847158",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Payroll",
|
"module": "Payroll",
|
||||||
"name": "Salary Detail",
|
"name": "Salary Detail",
|
||||||
|
@ -7,12 +7,12 @@ import datetime, math
|
|||||||
|
|
||||||
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
|
from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
|
||||||
from frappe.model.naming import make_autoname
|
from frappe.model.naming import make_autoname
|
||||||
from frappe.utils.background_jobs import enqueue
|
|
||||||
|
|
||||||
from frappe import msgprint, _
|
from frappe import msgprint, _
|
||||||
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
|
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_start_end_dates
|
||||||
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
|
||||||
from erpnext.utilities.transaction_base import TransactionBase
|
from erpnext.utilities.transaction_base import TransactionBase
|
||||||
|
from frappe.utils.background_jobs import enqueue
|
||||||
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
||||||
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
|
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
|
||||||
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
|
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
|
||||||
@ -618,8 +618,7 @@ class SalarySlip(TransactionBase):
|
|||||||
get_salary_component_data(additional_salary.component),
|
get_salary_component_data(additional_salary.component),
|
||||||
additional_salary.amount,
|
additional_salary.amount,
|
||||||
component_type,
|
component_type,
|
||||||
additional_salary,
|
additional_salary
|
||||||
is_recurring = additional_salary.is_recurring
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def add_tax_components(self, payroll_period):
|
def add_tax_components(self, payroll_period):
|
||||||
@ -640,7 +639,7 @@ class SalarySlip(TransactionBase):
|
|||||||
tax_row = get_salary_component_data(d)
|
tax_row = get_salary_component_data(d)
|
||||||
self.update_component_row(tax_row, tax_amount, "deductions")
|
self.update_component_row(tax_row, tax_amount, "deductions")
|
||||||
|
|
||||||
def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
|
def update_component_row(self, component_data, amount, component_type, additional_salary=None):
|
||||||
component_row = None
|
component_row = None
|
||||||
for d in self.get(component_type):
|
for d in self.get(component_type):
|
||||||
if d.salary_component != component_data.salary_component:
|
if d.salary_component != component_data.salary_component:
|
||||||
@ -719,7 +718,6 @@ class SalarySlip(TransactionBase):
|
|||||||
# get remaining numbers of sub-period (period for which one salary is processed)
|
# get remaining numbers of sub-period (period for which one salary is processed)
|
||||||
remaining_sub_periods = get_period_factor(self.employee,
|
remaining_sub_periods = get_period_factor(self.employee,
|
||||||
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
|
self.start_date, self.end_date, self.payroll_frequency, payroll_period)[1]
|
||||||
|
|
||||||
# get taxable_earnings, paid_taxes for previous period
|
# get taxable_earnings, paid_taxes for previous period
|
||||||
previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
|
previous_taxable_earnings = self.get_taxable_earnings_for_prev_period(payroll_period.start_date,
|
||||||
self.start_date, tax_slab.allow_tax_exemption)
|
self.start_date, tax_slab.allow_tax_exemption)
|
||||||
@ -879,16 +877,8 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
if earning.is_tax_applicable:
|
if earning.is_tax_applicable:
|
||||||
if additional_amount:
|
if additional_amount:
|
||||||
if not earning.is_recurring_additional_salary:
|
taxable_earnings += (amount - additional_amount)
|
||||||
taxable_earnings += (amount - additional_amount)
|
additional_income += additional_amount
|
||||||
additional_income += additional_amount
|
|
||||||
else:
|
|
||||||
to_date = frappe.db.get_value("Additional Salary", earning.additional_salary, 'to_date')
|
|
||||||
period = (getdate(to_date).month - getdate(self.start_date).month) + 1
|
|
||||||
if period > 0:
|
|
||||||
taxable_earnings += (amount - additional_amount) * period
|
|
||||||
additional_income += additional_amount * period
|
|
||||||
|
|
||||||
if earning.deduct_full_tax_on_selected_payroll_date:
|
if earning.deduct_full_tax_on_selected_payroll_date:
|
||||||
additional_income_with_full_tax += additional_amount
|
additional_income_with_full_tax += additional_amount
|
||||||
continue
|
continue
|
||||||
|
@ -310,6 +310,7 @@
|
|||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"default": "1",
|
||||||
"fieldname": "exchange_rate",
|
"fieldname": "exchange_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "Exchange Rate"
|
"label": "Exchange Rate"
|
||||||
@ -319,7 +320,7 @@
|
|||||||
"idx": 1,
|
"idx": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-05-18 16:10:08.249619",
|
"modified": "2021-06-09 12:08:53.930200",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Projects",
|
"module": "Projects",
|
||||||
"name": "Timesheet",
|
"name": "Timesheet",
|
||||||
|
@ -227,7 +227,8 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
|
|||||||
return frappe.db.sql("""SELECT tsd.name as name,
|
return frappe.db.sql("""SELECT tsd.name as name,
|
||||||
tsd.parent as parent, tsd.billing_hours as billing_hours,
|
tsd.parent as parent, tsd.billing_hours as billing_hours,
|
||||||
tsd.billing_amount as billing_amount, tsd.activity_type as activity_type,
|
tsd.billing_amount as billing_amount, tsd.activity_type as activity_type,
|
||||||
tsd.description as description, ts.currency as currency
|
tsd.description as description, ts.currency as currency,
|
||||||
|
tsd.project_name as project_name
|
||||||
FROM `tabTimesheet Detail` tsd
|
FROM `tabTimesheet Detail` tsd
|
||||||
INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent
|
INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent
|
||||||
WHERE tsd.parenttype = 'Timesheet'
|
WHERE tsd.parenttype = 'Timesheet'
|
||||||
@ -235,6 +236,19 @@ def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to
|
|||||||
and tsd.is_billable = 1
|
and tsd.is_billable = 1
|
||||||
and tsd.sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
|
and tsd.sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_timesheet_detail_rate(timelog, currency):
|
||||||
|
timelog_detail = frappe.db.sql("""SELECT tsd.billing_amount as billing_amount,
|
||||||
|
ts.currency as currency FROM `tabTimesheet Detail` tsd
|
||||||
|
INNER JOIN `tabTimesheet` ts ON ts.name=tsd.parent
|
||||||
|
WHERE tsd.name = '{0}'""".format(timelog), as_dict = 1)[0]
|
||||||
|
|
||||||
|
if timelog_detail.currency:
|
||||||
|
exchange_rate = get_exchange_rate(timelog_detail.currency, currency)
|
||||||
|
|
||||||
|
return timelog_detail.billing_amount * exchange_rate
|
||||||
|
return timelog_detail.billing_amount
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
@frappe.validate_and_sanitize_search_inputs
|
@frappe.validate_and_sanitize_search_inputs
|
||||||
def get_timesheet(doctype, txt, searchfield, start, page_len, filters):
|
def get_timesheet(doctype, txt, searchfield, start, page_len, filters):
|
||||||
|
@ -47,7 +47,10 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
|
|
||||||
if (in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_pos &&
|
if (in_list(["Sales Invoice", "POS Invoice"], this.frm.doc.doctype) && this.frm.doc.is_pos &&
|
||||||
this.frm.doc.is_return) {
|
this.frm.doc.is_return) {
|
||||||
this.update_paid_amount_for_return();
|
if (this.frm.doc.doctype == "Sales Invoice") {
|
||||||
|
this.set_total_amount_to_default_mop();
|
||||||
|
}
|
||||||
|
this.calculate_paid_amount();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sales person's commission
|
// Sales person's commission
|
||||||
@ -67,8 +70,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
|
|
||||||
calculate_discount_amount() {
|
calculate_discount_amount() {
|
||||||
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
|
if (frappe.meta.get_docfield(this.frm.doc.doctype, "discount_amount")) {
|
||||||
this.calculate_item_values();
|
|
||||||
this.calculate_net_total();
|
|
||||||
this.set_discount_amount();
|
this.set_discount_amount();
|
||||||
this.apply_discount_amount();
|
this.apply_discount_amount();
|
||||||
}
|
}
|
||||||
@ -734,7 +735,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
update_paid_amount_for_return() {
|
set_total_amount_to_default_mop() {
|
||||||
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
var grand_total = this.frm.doc.rounded_total || this.frm.doc.grand_total;
|
||||||
|
|
||||||
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
|
if(this.frm.doc.party_account_currency == this.frm.doc.currency) {
|
||||||
@ -747,7 +748,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
|||||||
precision("base_grand_total")
|
precision("base_grand_total")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.frm.doc.payments.find(pay => {
|
this.frm.doc.payments.find(pay => {
|
||||||
if (pay.default) {
|
if (pay.default) {
|
||||||
pay.amount = total_amount_to_pay;
|
pay.amount = total_amount_to_pay;
|
||||||
|
@ -1,78 +1,31 @@
|
|||||||
// 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
|
||||||
|
|
||||||
cur_frm.cscript.refresh = function(doc, dt, dn) {
|
frappe.ui.form.on("Email Digest", {
|
||||||
doc = locals[dt][dn];
|
refresh: function(frm) {
|
||||||
cur_frm.add_custom_button(__('View Now'), function() {
|
if (!frm.is_new()) {
|
||||||
frappe.call({
|
frm.add_custom_button(__('View Now'), function() {
|
||||||
method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg',
|
frappe.call({
|
||||||
args: {
|
method: 'erpnext.setup.doctype.email_digest.email_digest.get_digest_msg',
|
||||||
name: doc.name
|
args: {
|
||||||
},
|
name: frm.doc.name
|
||||||
callback: function(r) {
|
},
|
||||||
var d = new frappe.ui.Dialog({
|
callback: function(r) {
|
||||||
title: __('Email Digest: ') + dn,
|
let d = new frappe.ui.Dialog({
|
||||||
width: 800
|
title: __('Email Digest: {0}', [frm.doc.name]),
|
||||||
|
width: 800
|
||||||
|
});
|
||||||
|
$(d.body).html(r.message);
|
||||||
|
d.show();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
$(d.body).html(r.message);
|
|
||||||
d.show();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, "fa fa-eye-open", "btn-default");
|
|
||||||
|
|
||||||
if (!cur_frm.is_new()) {
|
|
||||||
cur_frm.add_custom_button(__('Send Now'), function() {
|
|
||||||
return cur_frm.call('send', null, (r) => {
|
|
||||||
frappe.show_alert(__('Message Sent'));
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
frm.add_custom_button(__('Send Now'), function() {
|
||||||
|
return frm.call('send', null, () => {
|
||||||
|
frappe.show_alert({ message: __("Message Sent"), indicator: 'green'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
cur_frm.cscript.addremove_recipients = function(doc, dt, dn) {
|
|
||||||
// Get user list
|
|
||||||
|
|
||||||
return cur_frm.call('get_users', null, function(r) {
|
|
||||||
// Open a dialog and display checkboxes against email addresses
|
|
||||||
doc = locals[dt][dn];
|
|
||||||
var d = new frappe.ui.Dialog({
|
|
||||||
title: __('Add/Remove Recipients'),
|
|
||||||
width: 400
|
|
||||||
});
|
|
||||||
|
|
||||||
$.each(r.user_list, function(i, v) {
|
|
||||||
var fullname = frappe.user.full_name(v.name);
|
|
||||||
if(fullname !== v.name) fullname = fullname + " <" + v.name + ">";
|
|
||||||
|
|
||||||
if(v.enabled==0) {
|
|
||||||
fullname = repl("<span style='color: red'> %(name)s (" + __("disabled user") + ")</span>", {name: v.name});
|
|
||||||
}
|
|
||||||
|
|
||||||
$('<div class="checkbox"><label>\
|
|
||||||
<input type="checkbox" data-id="' + v.name + '"'+
|
|
||||||
(v.checked ? 'checked' : '') +
|
|
||||||
'> '+ fullname +'</label></div>').appendTo(d.body);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Display add recipients button
|
|
||||||
d.set_primary_action("Update", function() {
|
|
||||||
cur_frm.cscript.add_to_rec_list(doc, d.body, r.user_list.length);
|
|
||||||
});
|
|
||||||
|
|
||||||
cur_frm.rec_dialog = d;
|
|
||||||
d.show();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
cur_frm.cscript.add_to_rec_list = function(doc, dialog, length) {
|
|
||||||
// add checked users to list of recipients
|
|
||||||
var rec_list = [];
|
|
||||||
$(dialog).find('input:checked').each(function(i, input) {
|
|
||||||
rec_list.push($(input).attr('data-id'));
|
|
||||||
});
|
|
||||||
|
|
||||||
doc.recipient_list = rec_list.join('\n');
|
|
||||||
cur_frm.rec_dialog.hide();
|
|
||||||
cur_frm.save();
|
|
||||||
cur_frm.refresh_fields();
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -47,19 +47,13 @@ class EmailDigest(Document):
|
|||||||
# send email only to enabled users
|
# send email only to enabled users
|
||||||
valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser`
|
valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser`
|
||||||
where enabled=1""")]
|
where enabled=1""")]
|
||||||
recipients = list(filter(lambda r: r in valid_users,
|
|
||||||
self.recipient_list.split("\n")))
|
|
||||||
|
|
||||||
original_user = frappe.session.user
|
if self.recipients:
|
||||||
|
for row in self.recipients:
|
||||||
if recipients:
|
|
||||||
for user_id in recipients:
|
|
||||||
frappe.set_user(user_id)
|
|
||||||
frappe.set_user_lang(user_id)
|
|
||||||
msg_for_this_recipient = self.get_msg_html()
|
msg_for_this_recipient = self.get_msg_html()
|
||||||
if msg_for_this_recipient:
|
if msg_for_this_recipient and row.recipient in valid_users:
|
||||||
frappe.sendmail(
|
frappe.sendmail(
|
||||||
recipients=user_id,
|
recipients=row.recipient,
|
||||||
subject=_("{0} Digest").format(self.frequency),
|
subject=_("{0} Digest").format(self.frequency),
|
||||||
message=msg_for_this_recipient,
|
message=msg_for_this_recipient,
|
||||||
reference_doctype = self.doctype,
|
reference_doctype = self.doctype,
|
||||||
|
@ -0,0 +1,33 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"creation": "2020-06-08 12:19:40.428949",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"recipient"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "recipient",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Recipient",
|
||||||
|
"options": "User",
|
||||||
|
"reqd": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2020-08-24 23:10:23.217572",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Setup",
|
||||||
|
"name": "Email Digest Recipient",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"quick_entry": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"track_changes": 1
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
class EmailDigestRecipient(Document):
|
||||||
|
pass
|
@ -45,9 +45,16 @@ def enable_shopping_cart(args):
|
|||||||
def create_email_digest():
|
def create_email_digest():
|
||||||
from frappe.utils.user import get_system_managers
|
from frappe.utils.user import get_system_managers
|
||||||
system_managers = get_system_managers(only_name=True)
|
system_managers = get_system_managers(only_name=True)
|
||||||
|
|
||||||
if not system_managers:
|
if not system_managers:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
recipients = []
|
||||||
|
for d in system_managers:
|
||||||
|
recipients.append({
|
||||||
|
'recipient': d
|
||||||
|
})
|
||||||
|
|
||||||
companies = frappe.db.sql_list("select name FROM `tabCompany`")
|
companies = frappe.db.sql_list("select name FROM `tabCompany`")
|
||||||
for company in companies:
|
for company in companies:
|
||||||
if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company):
|
if not frappe.db.exists("Email Digest", "Default Weekly Digest - " + company):
|
||||||
@ -56,7 +63,7 @@ def create_email_digest():
|
|||||||
"name": "Default Weekly Digest - " + company,
|
"name": "Default Weekly Digest - " + company,
|
||||||
"company": company,
|
"company": company,
|
||||||
"frequency": "Weekly",
|
"frequency": "Weekly",
|
||||||
"recipient_list": "\n".join(system_managers)
|
"recipients": recipients
|
||||||
})
|
})
|
||||||
|
|
||||||
for df in edigest.meta.get("fields", {"fieldtype": "Check"}):
|
for df in edigest.meta.get("fields", {"fieldtype": "Check"}):
|
||||||
@ -72,7 +79,7 @@ def create_email_digest():
|
|||||||
"name": "Scheduler Errors",
|
"name": "Scheduler Errors",
|
||||||
"company": companies[0],
|
"company": companies[0],
|
||||||
"frequency": "Daily",
|
"frequency": "Daily",
|
||||||
"recipient_list": "\n".join(system_managers),
|
"recipients": recipients,
|
||||||
"scheduler_errors": 1,
|
"scheduler_errors": 1,
|
||||||
"enabled": 1
|
"enabled": 1
|
||||||
})
|
})
|
||||||
|
@ -326,7 +326,6 @@ class TestPurchaseReceipt(unittest.TestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, pr2.submit)
|
self.assertRaises(frappe.ValidationError, pr2.submit)
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
|
||||||
def test_serial_no_supplier(self):
|
def test_serial_no_supplier(self):
|
||||||
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
|
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
|
||||||
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
|
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
|
||||||
|
@ -116,7 +116,7 @@ class Issue(Document):
|
|||||||
}).insert(ignore_permissions=True)
|
}).insert(ignore_permissions=True)
|
||||||
|
|
||||||
return replicated_issue.name
|
return replicated_issue.name
|
||||||
|
|
||||||
def reset_issue_metrics(self):
|
def reset_issue_metrics(self):
|
||||||
self.db_set("resolution_time", None)
|
self.db_set("resolution_time", None)
|
||||||
self.db_set("user_resolution_time", None)
|
self.db_set("user_resolution_time", None)
|
||||||
@ -231,7 +231,7 @@ def set_first_response_time(communication, method):
|
|||||||
|
|
||||||
def is_first_response(issue):
|
def is_first_response(issue):
|
||||||
responses = frappe.get_all('Communication', filters = {'reference_name': issue.name, 'sent_or_received': 'Sent'})
|
responses = frappe.get_all('Communication', filters = {'reference_name': issue.name, 'sent_or_received': 'Sent'})
|
||||||
if len(responses) == 1:
|
if len(responses) == 1:
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -260,7 +260,7 @@ def calculate_first_response_time(issue, first_responded_on):
|
|||||||
# both issue creation and first response were after working hours
|
# both issue creation and first response were after working hours
|
||||||
else:
|
else:
|
||||||
return 1.0 # this should ideally be zero, but it gets reset when the next response is sent if the value is zero
|
return 1.0 # this should ideally be zero, but it gets reset when the next response is sent if the value is zero
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return 1.0
|
return 1.0
|
||||||
|
|
||||||
|
@ -18,6 +18,10 @@
|
|||||||
"entity_type",
|
"entity_type",
|
||||||
"column_break_10",
|
"column_break_10",
|
||||||
"entity",
|
"entity",
|
||||||
|
"filters_section",
|
||||||
|
"condition",
|
||||||
|
"column_break_15",
|
||||||
|
"condition_description",
|
||||||
"agreement_details_section",
|
"agreement_details_section",
|
||||||
"start_date",
|
"start_date",
|
||||||
"column_break_7",
|
"column_break_7",
|
||||||
@ -176,10 +180,34 @@
|
|||||||
"fieldname": "apply_sla_for_resolution",
|
"fieldname": "apply_sla_for_resolution",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Apply SLA for Resolution Time"
|
"label": "Apply SLA for Resolution Time"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Pause SLA On",
|
||||||
|
"options": "Pause SLA On Status"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "filters_section",
|
||||||
|
"fieldtype": "Section Break",
|
||||||
|
"label": "Assignment Condition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "column_break_15",
|
||||||
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "condition",
|
||||||
|
"fieldtype": "Code",
|
||||||
|
"label": "Condition",
|
||||||
|
"options": "Python"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "condition_description",
|
||||||
|
"fieldtype": "HTML",
|
||||||
|
"options": "<p><strong>Condition Examples:</strong></p>\n<pre>doc.status==\"Open\"<br>doc.due_date==nowdate()<br>doc.total > 40000\n</pre>"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-07-08 12:28:46.283334",
|
"modified": "2021-07-09 12:28:46.283334",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Support",
|
"module": "Support",
|
||||||
"name": "Service Level Agreement",
|
"name": "Service Level Agreement",
|
||||||
@ -213,4 +241,4 @@
|
|||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
|
@ -3,14 +3,16 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
from __future__ import unicode_literals
|
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.core.utils import get_parent_doc
|
from frappe.core.utils import get_parent_doc
|
||||||
from frappe.utils import time_diff_in_seconds, getdate, get_weekdays, add_to_date, get_time, get_datetime, \
|
from frappe.utils import time_diff_in_seconds, getdate, get_weekdays, add_to_date, get_time, get_datetime, \
|
||||||
get_time_zone, to_timedelta, get_datetime_str, get_link_to_form, cint
|
get_time_zone, to_timedelta, get_datetime_str, get_link_to_form, cint, nowdate
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from erpnext.support.doctype.issue.issue import get_holidays
|
from erpnext.support.doctype.issue.issue import get_holidays
|
||||||
|
from frappe.utils.safe_exec import get_safe_globals
|
||||||
|
|
||||||
class ServiceLevelAgreement(Document):
|
class ServiceLevelAgreement(Document):
|
||||||
def validate(self):
|
def validate(self):
|
||||||
@ -18,6 +20,7 @@ class ServiceLevelAgreement(Document):
|
|||||||
self.validate_status_field()
|
self.validate_status_field()
|
||||||
self.check_priorities()
|
self.check_priorities()
|
||||||
self.check_support_and_resolution()
|
self.check_support_and_resolution()
|
||||||
|
self.validate_condition()
|
||||||
|
|
||||||
def check_priorities(self):
|
def check_priorities(self):
|
||||||
priorities = []
|
priorities = []
|
||||||
@ -96,6 +99,14 @@ class ServiceLevelAgreement(Document):
|
|||||||
frappe.throw(_("The Document Type {0} must have a Status field to configure Service Level Agreement").format(
|
frappe.throw(_("The Document Type {0} must have a Status field to configure Service Level Agreement").format(
|
||||||
frappe.bold(self.document_type)))
|
frappe.bold(self.document_type)))
|
||||||
|
|
||||||
|
def validate_condition(self):
|
||||||
|
temp_doc = frappe.new_doc('Issue')
|
||||||
|
if self.condition:
|
||||||
|
try:
|
||||||
|
frappe.safe_eval(self.condition, None, get_context(temp_doc))
|
||||||
|
except Exception:
|
||||||
|
frappe.throw(_("The Condition '{0}' is invalid").format(self.condition))
|
||||||
|
|
||||||
def get_service_level_agreement_priority(self, priority):
|
def get_service_level_agreement_priority(self, priority):
|
||||||
priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name})
|
priority = frappe.get_doc("Service Level Priority", {"priority": priority, "parent": self.name})
|
||||||
|
|
||||||
@ -204,34 +215,51 @@ def check_agreement_status():
|
|||||||
if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()):
|
if doc.end_date and getdate(doc.end_date) < getdate(frappe.utils.getdate()):
|
||||||
frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "enabled", 0)
|
frappe.db.set_value("Service Level Agreement", service_level_agreement.name, "enabled", 0)
|
||||||
|
|
||||||
|
def get_active_service_level_agreement_for(doc):
|
||||||
def get_active_service_level_agreement_for(doctype, priority, customer=None, service_level_agreement=None):
|
if not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
|
||||||
if doctype == "Issue" and not frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
filters = [
|
filters = [
|
||||||
["Service Level Agreement", "document_type", "=", doctype],
|
["Service Level Agreement", "document_type", "=", doctype],
|
||||||
["Service Level Agreement", "enabled", "=", 1]
|
["Service Level Agreement", "enabled", "=", 1]
|
||||||
]
|
]
|
||||||
if priority:
|
|
||||||
filters.append(["Service Level Priority", "priority", "=", priority])
|
|
||||||
|
|
||||||
or_filters = []
|
if doc.get('priority'):
|
||||||
|
filters.append(["Service Level Priority", "priority", "=", doc.get('priority')])
|
||||||
|
|
||||||
|
customer = doc.get('customer')
|
||||||
|
or_filters = [
|
||||||
|
["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]]
|
||||||
|
]
|
||||||
|
|
||||||
|
service_level_agreement = doc.get('service_level_agreement')
|
||||||
if service_level_agreement:
|
if service_level_agreement:
|
||||||
or_filters = [
|
or_filters = [
|
||||||
["Service Level Agreement", "name", "=", service_level_agreement],
|
["Service Level Agreement", "name", "=", doc.get('service_level_agreement')],
|
||||||
]
|
]
|
||||||
|
|
||||||
if customer:
|
default_sla_filter = filters + [["Service Level Agreement", "default_service_level_agreement", "=", 1]]
|
||||||
or_filters.append(
|
default_sla = frappe.get_all("Service Level Agreement", filters=default_sla_filter,
|
||||||
["Service Level Agreement", "entity", "in", [customer, get_customer_group(customer), get_customer_territory(customer)]]
|
fields=["name", "default_priority", "condition"])
|
||||||
)
|
|
||||||
or_filters.append(["Service Level Agreement", "default_service_level_agreement", "=", 1])
|
|
||||||
|
|
||||||
agreement = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters,
|
filters += [["Service Level Agreement", "default_service_level_agreement", "=", 0]]
|
||||||
fields=["name", "default_priority", "apply_sla_for_resolution"])
|
agreements = frappe.get_all("Service Level Agreement", filters=filters, or_filters=or_filters,
|
||||||
|
fields=["name", "default_priority", "condition"])
|
||||||
|
|
||||||
return agreement[0] if agreement else None
|
# check if the current document on which SLA is to be applied fulfills all the conditions
|
||||||
|
filtered_agreements = []
|
||||||
|
for agreement in agreements:
|
||||||
|
condition = agreement.get('condition')
|
||||||
|
if not condition or (condition and frappe.safe_eval(condition, None, get_context(doc))):
|
||||||
|
filtered_agreements.append(agreement)
|
||||||
|
|
||||||
|
# if any default sla
|
||||||
|
filtered_agreements += default_sla
|
||||||
|
|
||||||
|
return filtered_agreements[0] if filtered_agreements else None
|
||||||
|
|
||||||
|
def get_context(doc):
|
||||||
|
return {"doc": doc.as_dict(), "nowdate": nowdate, "frappe": frappe._dict(utils=get_safe_globals().get("frappe").get("utils"))}
|
||||||
|
|
||||||
|
|
||||||
def get_customer_group(customer):
|
def get_customer_group(customer):
|
||||||
@ -301,8 +329,7 @@ def apply(doc, method=None):
|
|||||||
doc.doctype not in get_documents_with_active_service_level_agreement():
|
doc.doctype not in get_documents_with_active_service_level_agreement():
|
||||||
return
|
return
|
||||||
|
|
||||||
service_level_agreement = get_active_service_level_agreement_for(doctype=doc.get("doctype"), priority=doc.get("priority"),
|
service_level_agreement = get_active_service_level_agreement_for(doc)
|
||||||
customer=doc.get("customer"), service_level_agreement=doc.get("service_level_agreement"))
|
|
||||||
|
|
||||||
if not service_level_agreement:
|
if not service_level_agreement:
|
||||||
return
|
return
|
||||||
|
Loading…
x
Reference in New Issue
Block a user