Merge branch 'develop' into fix-ignore-pricing-rule

This commit is contained in:
Saqib Ansari 2022-02-02 21:47:51 +05:30 committed by GitHub
commit 5a7236efcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
152 changed files with 5626 additions and 868 deletions

View File

@ -15,17 +15,6 @@ frappe.ui.form.on('Cost Center', {
} }
} }
}); });
frm.set_query("cost_center", "distributed_cost_center", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
enable_distributed_cost_center: 0,
name: ['!=', frm.doc.name]
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {
if (!frm.is_new()) { if (!frm.is_new()) {

View File

@ -16,9 +16,6 @@
"cb0", "cb0",
"is_group", "is_group",
"disabled", "disabled",
"section_break_9",
"enable_distributed_cost_center",
"distributed_cost_center",
"lft", "lft",
"rgt", "rgt",
"old_parent" "old_parent"
@ -122,31 +119,13 @@
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disabled" "label": "Disabled"
},
{
"default": "0",
"fieldname": "enable_distributed_cost_center",
"fieldtype": "Check",
"label": "Enable Distributed Cost Center"
},
{
"depends_on": "eval:doc.is_group==0",
"fieldname": "section_break_9",
"fieldtype": "Section Break"
},
{
"depends_on": "enable_distributed_cost_center",
"fieldname": "distributed_cost_center",
"fieldtype": "Table",
"label": "Distributed Cost Center",
"options": "Distributed Cost Center"
} }
], ],
"icon": "fa fa-money", "icon": "fa fa-money",
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2020-06-17 16:09:30.025214", "modified": "2022-01-31 13:22:58.916273",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Cost Center", "name": "Cost Center",
@ -189,5 +168,6 @@
"search_fields": "parent_cost_center, is_group", "search_fields": "parent_cost_center, is_group",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@ -4,7 +4,6 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import cint
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from erpnext.accounts.utils import validate_field_number from erpnext.accounts.utils import validate_field_number
@ -20,24 +19,6 @@ class CostCenter(NestedSet):
def validate(self): def validate(self):
self.validate_mandatory() self.validate_mandatory()
self.validate_parent_cost_center() self.validate_parent_cost_center()
self.validate_distributed_cost_center()
def validate_distributed_cost_center(self):
if cint(self.enable_distributed_cost_center):
if not self.distributed_cost_center:
frappe.throw(_("Please enter distributed cost center"))
if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
if not self.get('__islocal'):
if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
and self.check_if_part_of_distributed_cost_center():
frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
else:
self.distributed_cost_center = []
def validate_mandatory(self): def validate_mandatory(self):
if self.cost_center_name != self.company and not self.parent_cost_center: if self.cost_center_name != self.company and not self.parent_cost_center:
@ -64,10 +45,10 @@ class CostCenter(NestedSet):
@frappe.whitelist() @frappe.whitelist()
def convert_ledger_to_group(self): def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center): if self.if_allocation_exists_against_cost_center():
frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group")) frappe.throw(_("Cost Center with Allocation records can not be converted to a group"))
if self.check_if_part_of_distributed_cost_center(): if self.check_if_part_of_cost_center_allocation():
frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group")) frappe.throw(_("Cost Center is a part of Cost Center Allocation, hence cannot be converted to a group"))
if self.check_gle_exists(): if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group")) frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
self.is_group = 1 self.is_group = 1
@ -81,8 +62,17 @@ class CostCenter(NestedSet):
return frappe.db.sql("select name from `tabCost Center` where \ return frappe.db.sql("select name from `tabCost Center` where \
parent_cost_center = %s and docstatus != 2", self.name) parent_cost_center = %s and docstatus != 2", self.name)
def check_if_part_of_distributed_cost_center(self): def if_allocation_exists_against_cost_center(self):
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name}) return frappe.db.get_value("Cost Center Allocation", filters = {
"main_cost_center": self.name,
"docstatus": 1
})
def check_if_part_of_cost_center_allocation(self):
return frappe.db.get_value("Cost Center Allocation Percentage", filters = {
"cost_center": self.name,
"docstatus": 1
})
def before_rename(self, olddn, newdn, merge=False): def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided # Add company abbr if not provided
@ -126,8 +116,4 @@ def on_doctype_update():
def get_name_with_number(new_account, account_number): def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit(): if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account new_account = account_number + " - " + new_account
return new_account return new_account
def check_if_distributed_cost_center_enabled(cost_center_list):
value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
return next((True for x in value_list if x[0]), False)

View File

@ -23,33 +23,6 @@ class TestCostCenter(unittest.TestCase):
self.assertRaises(frappe.ValidationError, cost_center.save) self.assertRaises(frappe.ValidationError, cost_center.save)
def test_validate_distributed_cost_center(self):
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
frappe.get_doc(test_records[0]).insert()
if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
frappe.get_doc(test_records[1]).insert()
invalid_distributed_cost_center = frappe.get_doc({
"company": "_Test Company",
"cost_center_name": "_Test Distributed Cost Center",
"doctype": "Cost Center",
"is_group": 0,
"parent_cost_center": "_Test Company - _TC",
"enable_distributed_cost_center": 1,
"distributed_cost_center": [{
"cost_center": "_Test Cost Center - _TC",
"percentage_allocation": 40
}, {
"cost_center": "_Test Cost Center 2 - _TC",
"percentage_allocation": 50
}
]
})
self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
def create_cost_center(**args): def create_cost_center(**args):
args = frappe._dict(args) args = frappe._dict(args)
if args.cost_center_name: if args.cost_center_name:

View File

@ -0,0 +1,19 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cost Center Allocation', {
setup: function(frm) {
let filters = {"is_group": 0};
if (frm.doc.company) {
$.extend(filters, {
"company": frm.doc.company
});
}
frm.set_query('main_cost_center', function() {
return {
filters: filters
};
});
}
});

View File

@ -0,0 +1,128 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "CC-ALLOC-.#####",
"creation": "2022-01-13 20:07:29.871109",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"main_cost_center",
"company",
"column_break_2",
"valid_from",
"section_break_5",
"allocation_percentages",
"amended_from"
],
"fields": [
{
"fieldname": "main_cost_center",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Main Cost Center",
"options": "Cost Center",
"reqd": 1
},
{
"default": "Today",
"fieldname": "valid_from",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid From",
"reqd": 1
},
{
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fetch_from": "main_cost_center.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "allocation_percentages",
"fieldtype": "Table",
"label": "Cost Center Allocation Percentages",
"options": "Cost Center Allocation Percentage",
"reqd": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Cost Center Allocation",
"print_hide": 1,
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-01-31 11:47:12.086253",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cost Center Allocation",
"name_case": "UPPER CASE",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"submit": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,90 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import add_days, format_date, getdate
class MainCostCenterCantBeChild(frappe.ValidationError):
pass
class InvalidMainCostCenter(frappe.ValidationError):
pass
class InvalidChildCostCenter(frappe.ValidationError):
pass
class WrongPercentageAllocation(frappe.ValidationError):
pass
class InvalidDateError(frappe.ValidationError):
pass
class CostCenterAllocation(Document):
def validate(self):
self.validate_total_allocation_percentage()
self.validate_from_date_based_on_existing_gle()
self.validate_backdated_allocation()
self.validate_main_cost_center()
self.validate_child_cost_centers()
def validate_total_allocation_percentage(self):
total_percentage = sum([d.percentage for d in self.get("allocation_percentages", [])])
if total_percentage != 100:
frappe.throw(_("Total percentage against cost centers should be 100"), WrongPercentageAllocation)
def validate_from_date_based_on_existing_gle(self):
# Check if GLE exists against the main cost center
# If exists ensure from date is set after posting date of last GLE
last_gle_date = frappe.db.get_value("GL Entry",
{"cost_center": self.main_cost_center, "is_cancelled": 0},
"posting_date", order_by="posting_date desc")
if last_gle_date:
if getdate(self.valid_from) <= getdate(last_gle_date):
frappe.throw(_("Valid From must be after {0} as last GL Entry against the cost center {1} posted on this date")
.format(last_gle_date, self.main_cost_center), InvalidDateError)
def validate_backdated_allocation(self):
# Check if there are any future existing allocation records against the main cost center
# If exists, warn the user about it
future_allocation = frappe.db.get_value("Cost Center Allocation", filters = {
"main_cost_center": self.main_cost_center,
"valid_from": (">=", self.valid_from),
"name": ("!=", self.name),
"docstatus": 1
}, fieldname=['valid_from', 'name'], order_by='valid_from', as_dict=1)
if future_allocation:
frappe.msgprint(_("Another Cost Center Allocation record {0} applicable from {1}, hence this allocation will be applicable upto {2}")
.format(frappe.bold(future_allocation.name), frappe.bold(format_date(future_allocation.valid_from)),
frappe.bold(format_date(add_days(future_allocation.valid_from, -1)))),
title=_("Warning!"), indicator="orange", alert=1
)
def validate_main_cost_center(self):
# Main cost center itself cannot be entered in child table
if self.main_cost_center in [d.cost_center for d in self.allocation_percentages]:
frappe.throw(_("Main Cost Center {0} cannot be entered in the child table")
.format(self.main_cost_center), MainCostCenterCantBeChild)
# If main cost center is used for allocation under any other cost center,
# allocation cannot be done against it
parent = frappe.db.get_value("Cost Center Allocation Percentage", filters = {
"cost_center": self.main_cost_center,
"docstatus": 1
}, fieldname='parent')
if parent:
frappe.throw(_("{0} cannot be used as a Main Cost Center because it has been used as child in Cost Center Allocation {1}")
.format(self.main_cost_center, parent), InvalidMainCostCenter)
def validate_child_cost_centers(self):
# Check if child cost center is used as main cost center in any existing allocation
main_cost_centers = [d.main_cost_center for d in
frappe.get_all("Cost Center Allocation", {'docstatus': 1}, 'main_cost_center')]
for d in self.allocation_percentages:
if d.cost_center in main_cost_centers:
frappe.throw(_("Cost Center {0} cannot be used for allocation as it is used as main cost center in other allocation record.")
.format(d.cost_center), InvalidChildCostCenter)

View File

@ -0,0 +1,156 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
from frappe.utils import add_days, today
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.cost_center_allocation.cost_center_allocation import (
InvalidChildCostCenter,
InvalidDateError,
InvalidMainCostCenter,
MainCostCenterCantBeChild,
WrongPercentageAllocation,
)
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
class TestCostCenterAllocation(unittest.TestCase):
def setUp(self):
cost_centers = ["Main Cost Center 1", "Main Cost Center 2", "Sub Cost Center 1", "Sub Cost Center 2"]
for cc in cost_centers:
create_cost_center(cost_center_name=cc, company="_Test Company")
def test_gle_based_on_cost_center_allocation(self):
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
cost_center = "Main Cost Center 1 - _TC", submit=True)
expected_values = [
["Sub Cost Center 1 - _TC", 0.0, 60],
["Sub Cost Center 2 - _TC", 0.0, 40]
]
gle = frappe.qb.DocType("GL Entry")
gl_entries = (
frappe.qb.from_(gle)
.select(gle.cost_center, gle.debit, gle.credit)
.where(gle.voucher_type == 'Journal Entry')
.where(gle.voucher_no == jv.name)
.where(gle.account == 'Sales - _TC')
.orderby(gle.cost_center)
).run(as_dict=1)
self.assertTrue(gl_entries)
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.cost_center)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
cca.cancel()
jv.cancel()
def test_main_cost_center_cant_be_child(self):
# Main cost center itself cannot be entered in child table
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Main Cost Center 1 - _TC": 40
}, save=False
)
self.assertRaises(MainCostCenterCantBeChild, cca.save)
def test_invalid_main_cost_center(self):
# If main cost center is used for allocation under any other cost center,
# allocation cannot be done against it
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
cca2 = create_cost_center_allocation("_Test Company", "Sub Cost Center 1 - _TC",
{
"Sub Cost Center 2 - _TC": 100
}, save=False
)
self.assertRaises(InvalidMainCostCenter, cca2.save)
cca1.cancel()
def test_if_child_cost_center_has_any_allocation_record(self):
# Check if any child cost center is used as main cost center in any other existing allocation
cca1 = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}
)
cca2 = create_cost_center_allocation("_Test Company", "Main Cost Center 2 - _TC",
{
"Main Cost Center 1 - _TC": 60,
"Sub Cost Center 1 - _TC": 40
}, save=False
)
self.assertRaises(InvalidChildCostCenter, cca2.save)
cca1.cancel()
def test_total_percentage(self):
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 40,
"Sub Cost Center 2 - _TC": 40
}, save=False
)
self.assertRaises(WrongPercentageAllocation, cca.save)
def test_valid_from_based_on_existing_gle(self):
# GLE posted against Sub Cost Center 1 on today
jv = make_journal_entry("_Test Cash - _TC", "Sales - _TC", 100,
cost_center = "Main Cost Center 1 - _TC", posting_date=today(), submit=True)
# try to set valid from as yesterday
cca = create_cost_center_allocation("_Test Company", "Main Cost Center 1 - _TC",
{
"Sub Cost Center 1 - _TC": 60,
"Sub Cost Center 2 - _TC": 40
}, valid_from=add_days(today(), -1), save=False
)
self.assertRaises(InvalidDateError, cca.save)
jv.cancel()
def create_cost_center_allocation(company, main_cost_center, allocation_percentages,
valid_from=None, valid_upto=None, save=True, submit=True):
doc = frappe.new_doc("Cost Center Allocation")
doc.main_cost_center = main_cost_center
doc.company = company
doc.valid_from = valid_from or today()
doc.valid_upto = valid_upto
for cc, percentage in allocation_percentages.items():
doc.append("allocation_percentages", {
"cost_center": cc,
"percentage": percentage
})
if save:
doc.save()
if submit:
doc.submit()
return doc

View File

@ -1,12 +1,13 @@
{ {
"actions": [], "actions": [],
"creation": "2020-03-19 12:34:01.500390", "allow_rename": 1,
"creation": "2022-01-13 20:07:30.096306",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"cost_center", "cost_center",
"percentage_allocation" "percentage"
], ],
"fields": [ "fields": [
{ {
@ -18,23 +19,23 @@
"reqd": 1 "reqd": 1
}, },
{ {
"fieldname": "percentage_allocation", "fieldname": "percentage",
"fieldtype": "Float", "fieldtype": "Percent",
"in_list_view": 1, "in_list_view": 1,
"label": "Percentage Allocation", "label": "Percentage (%)",
"reqd": 1 "reqd": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-03-19 12:54:43.674655", "modified": "2022-02-01 22:22:31.589523",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Distributed Cost Center", "name": "Cost Center Allocation Percentage",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "states": []
} }

View File

@ -0,0 +1,9 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class CostCenterAllocationPercentage(Document):
pass

View File

@ -42,7 +42,6 @@ class POSInvoice(SalesInvoice):
self.validate_serialised_or_batched_item() self.validate_serialised_or_batched_item()
self.validate_stock_availablility() self.validate_stock_availablility()
self.validate_return_items_qty() self.validate_return_items_qty()
self.validate_non_stock_items()
self.set_status() self.set_status()
self.set_account_for_mode_of_payment() self.set_account_for_mode_of_payment()
self.validate_pos() self.validate_pos()
@ -158,22 +157,39 @@ class POSInvoice(SalesInvoice):
frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.") frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.")
.format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable"))
def validate_invalid_serial_nos(self, item):
serial_nos = get_serial_nos(item.serial_no)
error_msg = []
invalid_serials, msg = "", ""
for serial_no in serial_nos:
if not frappe.db.exists('Serial No', serial_no):
invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no
msg = (_("Row #{}: Following Serial numbers for item {} are <b>Invalid</b>: {}").format(item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials)))
if invalid_serials:
error_msg.append(msg)
if error_msg:
frappe.throw(error_msg, title=_("Invalid Item"), as_list=True)
def validate_stock_availablility(self): def validate_stock_availablility(self):
if self.is_return or self.docstatus != 1: if self.is_return or self.docstatus != 1:
return return
allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock')
for d in self.get('items'): for d in self.get('items'):
is_service_item = not (frappe.db.get_value('Item', d.get('item_code'), 'is_stock_item'))
if is_service_item:
return
if d.serial_no: if d.serial_no:
self.validate_pos_reserved_serial_nos(d) self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d) self.validate_delivered_serial_nos(d)
self.validate_invalid_serial_nos(d)
elif d.batch_no: elif d.batch_no:
self.validate_pos_reserved_batch_qty(d) self.validate_pos_reserved_batch_qty(d)
else: else:
if allow_negative_stock: if allow_negative_stock:
return return
available_stock = get_stock_availability(d.item_code, d.warehouse) available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse)
item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)
if flt(available_stock) <= 0: if flt(available_stock) <= 0:
@ -244,14 +260,6 @@ class POSInvoice(SalesInvoice):
.format(d.idx, bold_serial_no, bold_return_against) .format(d.idx, bold_serial_no, bold_return_against)
) )
def validate_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
if not is_stock_item:
if not frappe.db.exists('Product Bundle', d.item_code):
frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.")
.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item"))
def validate_mode_of_payment(self): def validate_mode_of_payment(self):
if len(self.payments) == 0: if len(self.payments) == 0:
frappe.throw(_("At least one mode of payment is required for POS invoice.")) frappe.throw(_("At least one mode of payment is required for POS invoice."))
@ -491,12 +499,18 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist() @frappe.whitelist()
def get_stock_availability(item_code, warehouse): def get_stock_availability(item_code, warehouse):
if frappe.db.get_value('Item', item_code, 'is_stock_item'): if frappe.db.get_value('Item', item_code, 'is_stock_item'):
is_stock_item = True
bin_qty = get_bin_qty(item_code, warehouse) bin_qty = get_bin_qty(item_code, warehouse)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty return bin_qty - pos_sales_qty, is_stock_item
else: else:
is_stock_item = False
if frappe.db.exists('Product Bundle', item_code): if frappe.db.exists('Product Bundle', item_code):
return get_bundle_availability(item_code, warehouse) return get_bundle_availability(item_code, warehouse), is_stock_item
else:
# Is a service item
return 0, is_stock_item
def get_bundle_availability(bundle_item_code, warehouse): def get_bundle_availability(bundle_item_code, warehouse):
product_bundle = frappe.get_doc('Product Bundle', bundle_item_code) product_bundle = frappe.get_doc('Product Bundle', bundle_item_code)

View File

@ -354,6 +354,24 @@ class TestPOSInvoice(unittest.TestCase):
pos2.insert() pos2.insert()
self.assertRaises(frappe.ValidationError, pos2.submit) self.assertRaises(frappe.ValidationError, pos2.submit)
def test_invalid_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item(company='_Test Company',
target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
serial_nos = se.get("items")[0].serial_no + 'wrong'
pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
item=se.get("items")[0].item_code, rate=1000, qty=2, do_not_save=1)
pos.get('items')[0].has_serial_no = 1
pos.get('items')[0].serial_no = serial_nos
pos.insert()
self.assertRaises(frappe.ValidationError, pos.submit)
def test_loyalty_points(self): def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points, get_loyalty_program_details_with_points,

View File

@ -548,6 +548,10 @@ class PurchaseInvoice(BuyingController):
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')) enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
provisional_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, \
'enable_provisional_accounting_for_non_stock_items'))
purchase_receipt_doc_map = {}
for item in self.get("items"): for item in self.get("items"):
if flt(item.base_net_amount): if flt(item.base_net_amount):
@ -643,19 +647,23 @@ class PurchaseInvoice(BuyingController):
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"))
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) if provisional_accounting_for_non_stock_items:
if auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
if item.purchase_receipt: if item.purchase_receipt:
provisional_account = self.get_company_default("default_provisional_account")
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
if not purchase_receipt_doc:
purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt)
purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc
# Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt
expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0, expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0,
'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail, 'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail,
'account':service_received_but_not_billed_account}, ['name']) 'account':provisional_account}, ['name'])
if expense_booked_in_pr: if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account # Intentionally passing purchase invoice item to handle partial billing
purchase_receipt_doc.add_provisional_gl_entry(item, gl_entries, self.posting_date, reverse=1)
if not self.is_internal_transfer(): if not self.is_internal_transfer():
gl_entries.append(self.get_gl_dict({ gl_entries.append(self.get_gl_dict({

View File

@ -11,12 +11,17 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today
import erpnext import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.controllers.buying_controller import QtyMismatchError
from erpnext.exceptions import InvalidCurrency from erpnext.exceptions import InvalidCurrency
from erpnext.projects.doctype.project.test_project import make_project from erpnext.projects.doctype.project.test_project import make_project
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as create_purchase_invoice_from_receipt,
)
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_taxes, get_taxes,
make_purchase_receipt, make_purchase_receipt,
@ -1147,8 +1152,6 @@ class TestPurchaseInvoice(unittest.TestCase):
def test_purchase_invoice_advance_taxes(self): def test_purchase_invoice_advance_taxes(self):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
# create a new supplier to test # create a new supplier to test
supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier', supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
@ -1221,6 +1224,45 @@ class TestPurchaseInvoice(unittest.TestCase):
payment_entry.load_from_db() payment_entry.load_from_db()
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
def test_provisional_accounting_entry(self):
item = create_item("_Test Non Stock Item", is_stock_item=0)
provisional_account = create_account(account_name="Provision Account",
parent_account="Current Liabilities - _TC", company="_Test Company")
company = frappe.get_doc('Company', '_Test Company')
company.enable_provisional_accounting_for_non_stock_items = 1
company.default_provisional_account = provisional_account
company.save()
pr = make_purchase_receipt(item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2))
pi = create_purchase_invoice_from_receipt(pr.name)
pi.set_posting_time = 1
pi.posting_date = add_days(pr.posting_date, -1)
pi.items[0].expense_account = 'Cost of Goods Sold - _TC'
pi.save()
pi.submit()
# Check GLE for Purchase Invoice
expected_gle = [
['Cost of Goods Sold - _TC', 250, 0, add_days(pr.posting_date, -1)],
['Creditors - _TC', 0, 250, add_days(pr.posting_date, -1)]
]
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
expected_gle_for_purchase_receipt = [
["Provision Account - _TC", 250, 0, pr.posting_date],
["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date],
["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date]
]
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
def check_gl_entries(doc, voucher_no, expected_gle, posting_date): def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry` from `tabGL Entry`

View File

@ -1,6 +1,8 @@
{% include "erpnext/regional/india/taxes.js" %} {% include "erpnext/regional/india/taxes.js" %}
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice'); erpnext.setup_auto_gst_taxation('Sales Invoice');
erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", { frappe.ui.form.on("Sales Invoice", {
setup: function(frm) { setup: function(frm) {

View File

@ -36,4 +36,139 @@ frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
}; };
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false); list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
const generate_irns = () => {
const docnames = list_view.get_checked_items(true);
if (docnames && docnames.length) {
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
args: { docnames },
freeze: true,
freeze_message: __('Generating E-Invoices...')
});
} else {
frappe.msgprint({
message: __('Please select at least one sales invoice to generate IRN'),
title: __('No Invoice Selected'),
indicator: 'red'
});
}
};
const cancel_irns = () => {
const docnames = list_view.get_checked_items(true);
const fields = [
{
"label": "Reason",
"fieldname": "reason",
"fieldtype": "Select",
"reqd": 1,
"default": "1-Duplicate",
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
},
{
"label": "Remark",
"fieldname": "remark",
"fieldtype": "Data",
"reqd": 1
}
];
const d = new frappe.ui.Dialog({
title: __("Cancel IRN"),
fields: fields,
primary_action: function() {
const data = d.get_values();
frappe.call({
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
args: {
doctype: list_view.doctype,
docnames,
reason: data.reason.split('-')[0],
remark: data.remark
},
freeze: true,
freeze_message: __('Cancelling E-Invoices...'),
});
d.hide();
},
primary_action_label: __('Submit')
});
d.show();
};
let einvoicing_enabled = false;
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
einvoicing_enabled = enabled;
});
list_view.$result.on("change", "input[type=checkbox]", () => {
if (einvoicing_enabled) {
const docnames = list_view.get_checked_items(true);
// show/hide e-invoicing actions when no sales invoices are checked
if (docnames && docnames.length) {
// prevent adding actions twice if e-invoicing action group already exists
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
}
} else {
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
}
}
});
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices generated successfully', [invoices.length]),
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to generate IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Generation Complete'),
indicator: 'orange'
});
}
});
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
const { failures, user, invoices } = data;
if (invoices.length != failures.length) {
frappe.msgprint({
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
if (failures && failures.length && user == frappe.session.user) {
let message = `
Failed to cancel IRNs for following ${failures.length} sales invoices:
<ul style="padding-left: 20px; padding-top: 5px;">
${failures.map(d => `<li>${d.docname}</li>`).join('')}
</ul>
`;
frappe.msgprint({
message: message,
title: __('Bulk E-Invoice Cancellation Complete'),
indicator: 'orange'
});
}
});
}; };

View File

@ -469,7 +469,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
let row = frappe.get_doc(d.doctype, d.name) let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail) set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
}); });
frm.trigger("calculate_timesheet_totals"); this.frm.trigger("calculate_timesheet_totals");
} }
} }
}; };

View File

@ -294,6 +294,8 @@ class SalesInvoice(SellingController):
def before_cancel(self): def before_cancel(self):
self.check_if_consolidated_invoice() self.check_if_consolidated_invoice()
super(SalesInvoice, self).before_cancel()
self.update_time_sheet(None) self.update_time_sheet(None)
def on_cancel(self): def on_cancel(self):

View File

@ -2100,6 +2100,54 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) self.assertEqual(data['billLists'][0]['actualFromStateCode'],7)
self.assertEqual(data['billLists'][0]['fromStateCode'],27) self.assertEqual(data['billLists'][0]['fromStateCode'],27)
def test_einvoice_submission_without_irn(self):
# init
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 1
einvoice_settings.applicable_from = nowdate()
einvoice_settings.append('credentials', {
'company': '_Test Company',
'gstin': '27AAECE4835E1ZR',
'username': 'test',
'password': 'test'
})
einvoice_settings.save()
country = frappe.flags.country
frappe.flags.country = 'India'
si = make_sales_invoice_for_ewaybill()
self.assertRaises(frappe.ValidationError, si.submit)
si.irn = 'test_irn'
si.submit()
# reset
einvoice_settings = frappe.get_doc('E Invoice Settings')
einvoice_settings.enable = 0
frappe.flags.country = country
def test_einvoice_json(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
si = get_sales_invoice_for_e_invoice()
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
self.assertTrue(einvoice['EwbDtls'])
validate_totals(einvoice)
si.apply_discount_on = 'Net Total'
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
[d.set('included_in_print_rate', 1) for d in si.taxes]
si.save()
einvoice = make_einvoice(si)
validate_totals(einvoice)
def test_item_tax_net_range(self): def test_item_tax_net_range(self):
item = create_item("T Shirt") item = create_item("T Shirt")

View File

@ -71,7 +71,8 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency: if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2) shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
self.add_shipping_rule_to_tax_table(doc, shipping_amount) if shipping_amount:
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value): def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"): for condition in self.get("conditions"):

View File

@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import copy
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
@ -51,49 +53,57 @@ def validate_accounting_period(gl_map):
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
def process_gl_map(gl_map, merge_entries=True, precision=None): def process_gl_map(gl_map, merge_entries=True, precision=None):
if not gl_map:
return []
gl_map = distribute_gl_based_on_cost_center_allocation(gl_map, precision)
if merge_entries: if merge_entries:
gl_map = merge_similar_entries(gl_map, precision) gl_map = merge_similar_entries(gl_map, precision)
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
entry.credit = flt(entry.credit) - flt(entry.debit)
entry.debit = 0.0
if flt(entry.debit_in_account_currency) < 0: gl_map = toggle_debit_credit_if_negative(gl_map)
entry.credit_in_account_currency = \
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
entry.debit_in_account_currency = 0.0
if flt(entry.credit) < 0:
entry.debit = flt(entry.debit) - flt(entry.credit)
entry.credit = 0.0
if flt(entry.credit_in_account_currency) < 0:
entry.debit_in_account_currency = \
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
entry.credit_in_account_currency = 0.0
update_net_values(entry)
return gl_map return gl_map
def update_net_values(entry): def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
# In some scenarios net value needs to be shown in the ledger cost_center_allocation = get_cost_center_allocation_data(gl_map[0]["company"], gl_map[0]["posting_date"])
# This method updates net values as debit or credit if not cost_center_allocation:
if entry.post_net_value and entry.debit and entry.credit: return gl_map
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = entry.debit_in_account_currency \
- entry.credit_in_account_currency
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = entry.credit_in_account_currency \
- entry.debit_in_account_currency
entry.debit = 0 new_gl_map = []
entry.debit_in_account_currency = 0 for d in gl_map:
cost_center = d.get("cost_center")
if cost_center and cost_center_allocation.get(cost_center):
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
gle = copy.deepcopy(d)
gle.cost_center = sub_cost_center
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
new_gl_map.append(gle)
else:
new_gl_map.append(d)
return new_gl_map
def get_cost_center_allocation_data(company, posting_date):
par = frappe.qb.DocType("Cost Center Allocation")
child = frappe.qb.DocType("Cost Center Allocation Percentage")
records = (
frappe.qb.from_(par).inner_join(child).on(par.name == child.parent)
.select(par.main_cost_center, child.cost_center, child.percentage)
.where(par.docstatus == 1)
.where(par.company == company)
.where(par.valid_from <= posting_date)
.orderby(par.valid_from, order=frappe.qb.desc)
).run(as_dict=True)
cc_allocation = frappe._dict()
for d in records:
cc_allocation.setdefault(d.main_cost_center, frappe._dict())\
.setdefault(d.cost_center, d.percentage)
return cc_allocation
def merge_similar_entries(gl_map, precision=None): def merge_similar_entries(gl_map, precision=None):
merged_gl_map = [] merged_gl_map = []
@ -145,6 +155,49 @@ def check_if_in_list(gle, gl_map, dimensions=None):
if same_head: if same_head:
return e return e
def toggle_debit_credit_if_negative(gl_map):
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
entry.credit = flt(entry.credit) - flt(entry.debit)
entry.debit = 0.0
if flt(entry.debit_in_account_currency) < 0:
entry.credit_in_account_currency = \
flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency)
entry.debit_in_account_currency = 0.0
if flt(entry.credit) < 0:
entry.debit = flt(entry.debit) - flt(entry.credit)
entry.credit = 0.0
if flt(entry.credit_in_account_currency) < 0:
entry.debit_in_account_currency = \
flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency)
entry.credit_in_account_currency = 0.0
update_net_values(entry)
return gl_map
def update_net_values(entry):
# In some scenarios net value needs to be shown in the ledger
# This method updates net values as debit or credit
if entry.post_net_value and entry.debit and entry.credit:
if entry.debit > entry.credit:
entry.debit = entry.debit - entry.credit
entry.debit_in_account_currency = entry.debit_in_account_currency \
- entry.credit_in_account_currency
entry.credit = 0
entry.credit_in_account_currency = 0
else:
entry.credit = entry.credit - entry.debit
entry.credit_in_account_currency = entry.credit_in_account_currency \
- entry.debit_in_account_currency
entry.debit = 0
entry.debit_in_account_currency = 0
def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
if not from_repost: if not from_repost:
validate_cwip_accounts(gl_map) validate_cwip_accounts(gl_map)

View File

@ -0,0 +1,173 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
<div class="page-break">
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
{% if letter_head and not no_letterhead %}
<div class="letter-head">{{ letter_head }}</div>
{% endif %}
<div class="print-heading">
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
</div>
</div>
{% if print_settings.repeat_header_footer %}
<div id="footer-html" class="visible-pdf">
{% if not no_letterhead and footer %}
<div class="letter-head-footer">
{{ footer }}
</div>
{% endif %}
<p class="text-center small page-number visible-pdf">
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
</p>
</div>
{% endif %}
<h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
<div class="col-xs-8 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. No</label></div>
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Ack. Date</label></div>
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Category</label></div>
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document Type</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
</div>
<div class="row data-field">
<div class="col-xs-4"><label>Document No</label></div>
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
</div>
</div>
<div class="col-xs-4 column-break">
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
</div>
</div>
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
<p>{{ seller.Gstin }}</p>
<p>{{ seller.LglNm }}</p>
<p>{{ seller.Addr1 }}</p>
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
<p>{{ seller.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
{%- if einvoice.ShipDtls -%}
{%- set shipping = einvoice.ShipDtls -%}
<h5 style="margin-bottom: 5px;">Shipped From</h5>
<p>{{ shipping.Gstin }}</p>
<p>{{ shipping.LglNm }}</p>
<p>{{ shipping.Addr1 }}</p>
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
<p>{{ shipping.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
{% endif %}
</div>
{%- set buyer = einvoice.BuyerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Buyer</h5>
<p>{{ buyer.Gstin }}</p>
<p>{{ buyer.LglNm }}</p>
<p>{{ buyer.Addr1 }}</p>
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
<p>{{ buyer.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
{%- if einvoice.DispDtls -%}
{%- set dispatch = einvoice.DispDtls -%}
<h5 style="margin-bottom: 5px;">Dispatched From</h5>
{%- if dispatch.Gstin -%} <p>{{ dispatch.Gstin }}</p> {% endif %}
<p>{{ dispatch.LglNm }}</p>
<p>{{ dispatch.Addr1 }}</p>
{%- if dispatch.Addr2 -%} <p>{{ dispatch.Addr2 }}</p> {% endif %}
<p>{{ dispatch.Loc }}</p>
<p>{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}</p>
{% endif %}
</div>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left" style="width: 3%;">Sr. No.</th>
<th class="text-left">Item</th>
<th class="text-left" style="width: 10%;">HSN Code</th>
<th class="text-left" style="width: 5%;">Qty</th>
<th class="text-left" style="width: 5%;">UOM</th>
<th class="text-left">Rate</th>
<th class="text-left" style="width: 5%;">Discount</th>
<th class="text-left">Taxable Amount</th>
<th class="text-left" style="width: 7%;">Tax Rate</th>
<th class="text-left" style="width: 5%;">Other Charges</th>
<th class="text-left">Total</th>
</tr>
</thead>
<tbody>
{% for item in einvoice.ItemList %}
<tr>
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
<td class="text-left">{{ item.PrdDesc }}</td>
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<div style="overflow-x: auto;">
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
<table class="table table-bordered">
<thead>
<tr>
<th class="text-left">Taxable Amount</th>
<th class="text-left">CGST</th>
<th class="text-left"">SGST</th>
<th class="text-left">IGST</th>
<th class="text-left">CESS</th>
<th class="text-left" style="width: 10%;">State CESS</th>
<th class="text-left">Discount</th>
<th class="text-left" style="width: 10%;">Other Charges</th>
<th class="text-left" style="width: 10%;">Round Off</th>
<th class="text-left">Total Value</th>
</tr>
</thead>
<tbody>
{%- set value_details = einvoice.ValDtls -%}
<tr>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
</tr>
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,24 @@
{
"align_labels_right": 1,
"creation": "2020-10-10 18:01:21.032914",
"custom_format": 0,
"default_print_language": "en-US",
"disabled": 1,
"doc_type": "Sales Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font": "Default",
"html": "",
"idx": 0,
"line_breaks": 1,
"modified": "2020-10-23 19:54:40.634936",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST E-Invoice",
"owner": "Administrator",
"print_format_builder": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 1,
"standard": "Yes"
}

View File

@ -29,18 +29,6 @@ def execute(filters=None):
dimension_items = cam_map.get(dimension) dimension_items = cam_map.get(dimension)
if dimension_items: if dimension_items:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0) data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0)
else:
DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(dimension)s
AND parent NOT IN %(dimension)s
GROUP BY parent''',{'dimension':[dimension]})
if DCC_allocation:
filters['budget_against_filter'] = [DCC_allocation[0][0]]
ddc_cam_map = get_dimension_account_month_map(filters)
dimension_items = ddc_cam_map.get(DCC_allocation[0][0])
if dimension_items:
data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1])
chart = get_chart_data(filters, columns, data) chart = get_chart_data(filters, columns, data)

View File

@ -387,42 +387,15 @@ def set_gl_entries_by_account(
key: value key: value
}) })
distributed_cost_center_query = "" gl_entries = frappe.db.sql("""
if filters and filters.get('cost_center'): select posting_date, account, debit, credit, is_opening, fiscal_year,
distributed_cost_center_query = """ debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
UNION ALL
SELECT posting_date,
account,
debit*(DCC_allocation.percentage_allocation/100) as debit,
credit*(DCC_allocation.percentage_allocation/100) as credit,
is_opening,
fiscal_year,
debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency,
account_currency
FROM `tabGL Entry`,
(
SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent
) as DCC_allocation
WHERE company=%(company)s
{additional_conditions}
AND posting_date <= %(to_date)s
AND is_cancelled = 0
AND cost_center = DCC_allocation.parent
""".format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry`
where company=%(company)s where company=%(company)s
{additional_conditions} {additional_conditions}
and posting_date <= %(to_date)s and posting_date <= %(to_date)s
and is_cancelled = 0 and is_cancelled = 0""".format(
{distributed_cost_center_query}""".format( additional_conditions=additional_conditions), gl_filters, as_dict=True
additional_conditions=additional_conditions, )
distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec
if filters and filters.get('presentation_currency'): if filters and filters.get('presentation_currency'):
convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company')) convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company'))

View File

@ -176,44 +176,7 @@ def get_gl_entries(filters, accounting_dimensions):
if accounting_dimensions: if accounting_dimensions:
dimension_fields = ', '.join(accounting_dimensions) + ',' dimension_fields = ', '.join(accounting_dimensions) + ','
distributed_cost_center_query = "" gl_entries = frappe.db.sql("""
if filters and filters.get('cost_center'):
select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit,
credit*(DCC_allocation.percentage_allocation/100) as credit,
debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
distributed_cost_center_query = """
UNION ALL
SELECT name as gl_entry,
posting_date,
account,
party_type,
party,
voucher_type,
voucher_no, {dimension_fields}
cost_center, project,
against_voucher_type,
against_voucher,
account_currency,
remarks, against,
is_opening, `tabGL Entry`.creation {select_fields_with_percentage}
FROM `tabGL Entry`,
(
SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent
) as DCC_allocation
WHERE company=%(company)s
{conditions}
AND posting_date <= %(to_date)s
AND cost_center = DCC_allocation.parent
""".format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ''))
gl_entries = frappe.db.sql(
"""
select select
name as gl_entry, posting_date, account, party_type, party, name as gl_entry, posting_date, account, party_type, party,
voucher_type, voucher_no, {dimension_fields} voucher_type, voucher_no, {dimension_fields}
@ -222,13 +185,11 @@ def get_gl_entries(filters, accounting_dimensions):
remarks, against, is_opening, creation {select_fields} remarks, against, is_opening, creation {select_fields}
from `tabGL Entry` from `tabGL Entry`
where company=%(company)s {conditions} where company=%(company)s {conditions}
{distributed_cost_center_query}
{order_by_statement} {order_by_statement}
""".format( """.format(
dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, dimension_fields=dimension_fields, select_fields=select_fields,
order_by_statement=order_by_statement conditions=get_conditions(filters), order_by_statement=order_by_statement
), ), filters, as_dict=1)
filters, as_dict=1)
if filters.get('presentation_currency'): if filters.get('presentation_currency'):
return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company'))

View File

@ -109,7 +109,6 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
def prepare_data(accounts, filters, total_row, parent_children_map, based_on): def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
data = [] data = []
new_accounts = accounts
company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency") company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency")
for d in accounts: for d in accounts:
@ -123,19 +122,6 @@ def prepare_data(accounts, filters, total_row, parent_children_map, based_on):
"currency": company_currency, "currency": company_currency,
"based_on": based_on "based_on": based_on
} }
if based_on == 'cost_center':
cost_center_doc = frappe.get_doc("Cost Center",d.name)
if not cost_center_doc.enable_distributed_cost_center:
DCC_allocation = frappe.db.sql("""SELECT parent, sum(percentage_allocation) as percentage_allocation
FROM `tabDistributed Cost Center`
WHERE cost_center IN %(cost_center)s
AND parent NOT IN %(cost_center)s
GROUP BY parent""",{'cost_center': [d.name]})
if DCC_allocation:
for account in new_accounts:
if account['name'] == DCC_allocation[0][0]:
for value in value_fields:
d[value] += account[value]*(DCC_allocation[0][1]/100)
for key in value_fields: for key in value_fields:
row[key] = flt(d.get(key, 0.0), 3) row[key] = flt(d.get(key, 0.0), 3)

View File

@ -1023,6 +1023,17 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"dependencies": "Cost Center",
"hidden": 0,
"is_query_report": 0,
"label": "Cost Center Allocation",
"link_count": 0,
"link_to": "Cost Center Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"dependencies": "Cost Center", "dependencies": "Cost Center",
"hidden": 0, "hidden": 0,

View File

@ -108,6 +108,10 @@ frappe.ui.form.on('Asset', {
frm.trigger("create_asset_repair"); frm.trigger("create_asset_repair");
}, __("Manage")); }, __("Manage"));
frm.add_custom_button(__("Split Asset"), function() {
frm.trigger("split_asset");
}, __("Manage"));
if (frm.doc.status != 'Fully Depreciated') { if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() { frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_value_adjustment"); frm.trigger("create_asset_value_adjustment");
@ -322,6 +326,43 @@ frappe.ui.form.on('Asset', {
}); });
}, },
split_asset: function(frm) {
const title = __('Split Asset');
const fields = [
{
fieldname: 'split_qty',
fieldtype: 'Int',
label: __('Split Qty'),
reqd: 1
}
];
let dialog = new frappe.ui.Dialog({
title: title,
fields: fields
});
dialog.set_primary_action(__('Split'), function() {
const dialog_data = dialog.get_values();
frappe.call({
args: {
"asset_name": frm.doc.name,
"split_qty": cint(dialog_data.split_qty)
},
method: "erpnext.assets.doctype.asset.asset.split_asset",
callback: function(r) {
let doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
dialog.hide();
});
dialog.show();
},
create_asset_value_adjustment: function(frm) { create_asset_value_adjustment: function(frm) {
frappe.call({ frappe.call({
args: { args: {

View File

@ -3,7 +3,7 @@
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2016-03-01 17:01:27.920130", "creation": "2022-01-18 02:26:55.975005",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Document", "document_type": "Document",
"engine": "InnoDB", "engine": "InnoDB",
@ -23,6 +23,7 @@
"asset_name", "asset_name",
"asset_category", "asset_category",
"location", "location",
"split_from",
"custodian", "custodian",
"department", "department",
"disposal_date", "disposal_date",
@ -142,6 +143,7 @@
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"fetch_from": "item_code.image",
"fieldname": "image", "fieldname": "image",
"fieldtype": "Attach Image", "fieldtype": "Attach Image",
"hidden": 1, "hidden": 1,
@ -482,6 +484,13 @@
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Finance Books" "label": "Finance Books"
}, },
{
"fieldname": "split_from",
"fieldtype": "Link",
"label": "Split From",
"options": "Asset",
"read_only": 1
},
{ {
"fieldname": "asset_quantity", "fieldname": "asset_quantity",
"fieldtype": "Int", "fieldtype": "Int",
@ -509,7 +518,7 @@
"link_fieldname": "asset" "link_fieldname": "asset"
} }
], ],
"modified": "2022-01-18 12:57:36.741192", "modified": "2022-01-30 20:19:24.680027",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset", "name": "Asset",

View File

@ -38,7 +38,8 @@ class Asset(AccountsController):
self.validate_item() self.validate_item()
self.validate_cost_center() self.validate_cost_center()
self.set_missing_values() self.set_missing_values()
self.prepare_depreciation_data() if not self.split_from:
self.prepare_depreciation_data()
self.validate_gross_and_purchase_amount() self.validate_gross_and_purchase_amount()
if self.get("schedules"): if self.get("schedules"):
self.validate_expected_value_after_useful_life() self.validate_expected_value_after_useful_life()
@ -202,143 +203,143 @@ class Asset(AccountsController):
start = self.clear_depreciation_schedule() start = self.clear_depreciation_schedule()
for finance_book in self.get('finance_books'): for finance_book in self.get('finance_books'):
self.validate_asset_finance_books(finance_book) self._make_depreciation_schedule(finance_book, start, date_of_sale)
# value_after_depreciation - current Asset value def _make_depreciation_schedule(self, finance_book, start, date_of_sale):
if self.docstatus == 1 and finance_book.value_after_depreciation: self.validate_asset_finance_books(finance_book)
value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
finance_book.value_after_depreciation = value_after_depreciation value_after_depreciation = self._get_value_after_depreciation(finance_book)
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \ number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked) cint(self.number_of_depreciations_booked)
has_pro_rata = self.check_is_pro_rata(finance_book) has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
if has_pro_rata: skip_row = False
number_of_pending_depreciations += 1
skip_row = False for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
for n in range(start[finance_book.idx-1], number_of_pending_depreciations): depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
# If depreciation is already completed (for double declining balance)
if skip_row: continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book) if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(finance_book.depreciation_start_date,
n * cint(finance_book.frequency_of_depreciation))
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1: # schedule date will be a year later from start date
schedule_date = add_months(finance_book.depreciation_start_date, # so monthly schedule date is calculated by removing 11 months from it
n * cint(finance_book.frequency_of_depreciation)) monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
# schedule date will be a year later from start date # if asset is being sold
# so monthly schedule date is calculated by removing 11 months from it if date_of_sale:
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1) from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
# if asset is being sold from_date, date_of_sale)
if date_of_sale:
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, date_of_sale)
if depreciation_amount > 0:
self.append("schedules", {
"schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount,
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
break
# For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, finance_book.depreciation_start_date)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount: continue
value_after_depreciation -= flt(depreciation_amount,
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0: if depreciation_amount > 0:
# With monthly depreciation, each depreciation is divided by months remaining until next date self._add_depreciation_row(date_of_sale, depreciation_amount, finance_book.depreciation_method,
if self.allow_monthly_depreciation: finance_book.finance_book, finance_book.idx)
# month range is 1 to 12
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else finance_book.frequency_of_depreciation
for r in range(month_range): break
if (has_pro_rata and n == 0):
# For first entry of monthly depr # For first row
if r == 0: if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date) from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
per_day_amt = depreciation_amount / days depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr from_date, finance_book.depreciation_start_date)
depreciation_amount -= depreciation_amount_for_current_month
date = monthly_schedule_date # For first depr schedule date will be the start date
amount = depreciation_amount_for_current_month # so monthly schedule date is calculated by removing month difference between use date and start date
else: monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / (month_range - 1) # For last row
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1: elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
# For last entry of monthly depr if not self.flags.increase_in_asset_life:
date = last_schedule_date # In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
amount = depreciation_amount / month_range self.to_date = add_months(self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date
if not depreciation_amount: continue
value_after_depreciation -= flt(depreciation_amount,
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0:
# With monthly depreciation, each depreciation is divided by months remaining until next date
if self.allow_monthly_depreciation:
# month range is 1 to 12
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else finance_book.frequency_of_depreciation
for r in range(month_range):
if (has_pro_rata and n == 0):
# For first entry of monthly depr
if r == 0:
days_until_first_depr = date_diff(monthly_schedule_date, self.available_for_use_date)
per_day_amt = depreciation_amount / days
depreciation_amount_for_current_month = per_day_amt * days_until_first_depr
depreciation_amount -= depreciation_amount_for_current_month
date = monthly_schedule_date
amount = depreciation_amount_for_current_month
else: else:
date = add_months(monthly_schedule_date, r) date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / month_range amount = depreciation_amount / (month_range - 1)
elif (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) and r == cint(month_range) - 1:
# For last entry of monthly depr
date = last_schedule_date
amount = depreciation_amount / month_range
else:
date = add_months(monthly_schedule_date, r)
amount = depreciation_amount / month_range
self.append("schedules", { self._add_depreciation_row(date, amount, finance_book.depreciation_method,
"schedule_date": date, finance_book.finance_book, finance_book.idx)
"depreciation_amount": amount, else:
"depreciation_method": finance_book.depreciation_method, self._add_depreciation_row(schedule_date, depreciation_amount, finance_book.depreciation_method,
"finance_book": finance_book.finance_book, finance_book.finance_book, finance_book.idx)
"finance_book_id": finance_book.idx
}) def _add_depreciation_row(self, schedule_date, depreciation_amount, depreciation_method, finance_book, finance_book_id):
else: self.append("schedules", {
self.append("schedules", { "schedule_date": schedule_date,
"schedule_date": schedule_date, "depreciation_amount": depreciation_amount,
"depreciation_amount": depreciation_amount, "depreciation_method": depreciation_method,
"depreciation_method": finance_book.depreciation_method, "finance_book": finance_book,
"finance_book": finance_book.finance_book, "finance_book_id": finance_book_id
"finance_book_id": finance_book.idx })
})
def _get_value_after_depreciation(self, finance_book):
# value_after_depreciation - current Asset value
if self.docstatus == 1 and finance_book.value_after_depreciation:
value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
return value_after_depreciation
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales # depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book # JE: Journal Entry, FB: Finance Book
@ -348,7 +349,6 @@ class Asset(AccountsController):
depr_schedule = [] depr_schedule = []
for schedule in self.get('schedules'): for schedule in self.get('schedules'):
# to update start when there are JEs linked with all the schedule rows corresponding to an FB # to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2): if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed) start.append(num_of_depreciations_completed)
@ -924,3 +924,113 @@ def get_depreciation_amount(asset, depreciable_value, row):
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
@frappe.whitelist()
def split_asset(asset_name, split_qty):
asset = frappe.get_doc("Asset", asset_name)
split_qty = cint(split_qty)
if split_qty >= asset.asset_quantity:
frappe.throw(_("Split qty cannot be grater than or equal to asset qty"))
remaining_qty = asset.asset_quantity - split_qty
new_asset = create_new_asset_after_split(asset, split_qty)
update_existing_asset(asset, remaining_qty)
return new_asset
def update_existing_asset(asset, remaining_qty):
remaining_gross_purchase_amount = flt((asset.gross_purchase_amount * remaining_qty) / asset.asset_quantity)
opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * remaining_qty) / asset.asset_quantity)
frappe.db.set_value("Asset", asset.name, {
'opening_accumulated_depreciation': opening_accumulated_depreciation,
'gross_purchase_amount': remaining_gross_purchase_amount,
'asset_quantity': remaining_qty
})
for finance_book in asset.get('finance_books'):
value_after_depreciation = flt((finance_book.value_after_depreciation * remaining_qty)/asset.asset_quantity)
expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * remaining_qty)/asset.asset_quantity)
frappe.db.set_value('Asset Finance Book', finance_book.name, 'value_after_depreciation', value_after_depreciation)
frappe.db.set_value('Asset Finance Book', finance_book.name, 'expected_value_after_useful_life', expected_value_after_useful_life)
accumulated_depreciation = 0
for term in asset.get('schedules'):
depreciation_amount = flt((term.depreciation_amount * remaining_qty)/asset.asset_quantity)
frappe.db.set_value('Depreciation Schedule', term.name, 'depreciation_amount', depreciation_amount)
accumulated_depreciation += depreciation_amount
frappe.db.set_value('Depreciation Schedule', term.name, 'accumulated_depreciation_amount', accumulated_depreciation)
def create_new_asset_after_split(asset, split_qty):
new_asset = frappe.copy_doc(asset)
new_gross_purchase_amount = flt((asset.gross_purchase_amount * split_qty) / asset.asset_quantity)
opening_accumulated_depreciation = flt((asset.opening_accumulated_depreciation * split_qty) / asset.asset_quantity)
new_asset.gross_purchase_amount = new_gross_purchase_amount
new_asset.opening_accumulated_depreciation = opening_accumulated_depreciation
new_asset.asset_quantity = split_qty
new_asset.split_from = asset.name
accumulated_depreciation = 0
for finance_book in new_asset.get('finance_books'):
finance_book.value_after_depreciation = flt((finance_book.value_after_depreciation * split_qty)/asset.asset_quantity)
finance_book.expected_value_after_useful_life = flt((finance_book.expected_value_after_useful_life * split_qty)/asset.asset_quantity)
for term in new_asset.get('schedules'):
depreciation_amount = flt((term.depreciation_amount * split_qty)/asset.asset_quantity)
term.depreciation_amount = depreciation_amount
accumulated_depreciation += depreciation_amount
term.accumulated_depreciation_amount = accumulated_depreciation
new_asset.submit()
new_asset.set_status()
for term in new_asset.get('schedules'):
# Update references in JV
if term.journal_entry:
add_reference_in_jv_on_split(term.journal_entry, new_asset.name, asset.name, term.depreciation_amount)
return new_asset
def add_reference_in_jv_on_split(entry_name, new_asset_name, old_asset_name, depreciation_amount):
journal_entry = frappe.get_doc('Journal Entry', entry_name)
entries_to_add = []
idx = len(journal_entry.get('accounts')) + 1
for account in journal_entry.get('accounts'):
if account.reference_name == old_asset_name:
entries_to_add.append(frappe.copy_doc(account).as_dict())
if account.credit:
account.credit = account.credit - depreciation_amount
account.credit_in_account_currency = account.credit_in_account_currency - \
account.exchange_rate * depreciation_amount
elif account.debit:
account.debit = account.debit - depreciation_amount
account.debit_in_account_currency = account.debit_in_account_currency - \
account.exchange_rate * depreciation_amount
for entry in entries_to_add:
entry.reference_name = new_asset_name
if entry.credit:
entry.credit = depreciation_amount
entry.credit_in_account_currency = entry.exchange_rate * depreciation_amount
elif entry.debit:
entry.debit = depreciation_amount
entry.debit_in_account_currency = entry.exchange_rate * depreciation_amount
entry.idx = idx
idx += 1
journal_entry.append('accounts', entry)
journal_entry.flags.ignore_validate_update_after_submit = True
journal_entry.save()
# Repost GL Entries
journal_entry.docstatus = 2
journal_entry.make_gl_entries(1)
journal_entry.docstatus = 1
journal_entry.make_gl_entries()

View File

@ -7,7 +7,7 @@ import frappe
from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate from frappe.utils import add_days, add_months, cstr, flt, get_last_day, getdate, nowdate
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import make_sales_invoice from erpnext.assets.doctype.asset.asset import make_sales_invoice, split_asset
from erpnext.assets.doctype.asset.depreciation import ( from erpnext.assets.doctype.asset.depreciation import (
post_depreciation_entries, post_depreciation_entries,
restore_asset, restore_asset,
@ -245,6 +245,57 @@ class TestAsset(AssetSetup):
si.cancel() si.cancel()
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated")
def test_asset_splitting(self):
asset = create_asset(
calculate_depreciation = 1,
asset_quantity=10,
available_for_use_date = '2020-01-01',
purchase_date = '2020-01-01',
expected_value_after_useful_life = 0,
total_number_of_depreciations = 6,
number_of_depreciations_booked = 1,
frequency_of_depreciation = 10,
depreciation_start_date = '2021-01-01',
opening_accumulated_depreciation=20000,
gross_purchase_amount=120000,
submit = 1
)
post_depreciation_entries(date="2021-01-01")
self.assertEqual(asset.asset_quantity, 10)
self.assertEqual(asset.gross_purchase_amount, 120000)
self.assertEqual(asset.opening_accumulated_depreciation, 20000)
new_asset = split_asset(asset.name, 2)
asset.load_from_db()
self.assertEqual(new_asset.asset_quantity, 2)
self.assertEqual(new_asset.gross_purchase_amount, 24000)
self.assertEqual(new_asset.opening_accumulated_depreciation, 4000)
self.assertEqual(new_asset.split_from, asset.name)
self.assertEqual(new_asset.schedules[0].depreciation_amount, 4000)
self.assertEqual(new_asset.schedules[1].depreciation_amount, 4000)
self.assertEqual(asset.asset_quantity, 8)
self.assertEqual(asset.gross_purchase_amount, 96000)
self.assertEqual(asset.opening_accumulated_depreciation, 16000)
self.assertEqual(asset.schedules[0].depreciation_amount, 16000)
self.assertEqual(asset.schedules[1].depreciation_amount, 16000)
journal_entry = asset.schedules[0].journal_entry
jv = frappe.get_doc('Journal Entry', journal_entry)
self.assertEqual(jv.accounts[0].credit_in_account_currency, 16000)
self.assertEqual(jv.accounts[1].debit_in_account_currency, 16000)
self.assertEqual(jv.accounts[2].credit_in_account_currency, 4000)
self.assertEqual(jv.accounts[3].debit_in_account_currency, 4000)
self.assertEqual(jv.accounts[0].reference_name, asset.name)
self.assertEqual(jv.accounts[1].reference_name, asset.name)
self.assertEqual(jv.accounts[2].reference_name, new_asset.name)
self.assertEqual(jv.accounts[3].reference_name, new_asset.name)
def test_expense_head(self): def test_expense_head(self):
pr = make_purchase_receipt(item_code="Macbook Pro", pr = make_purchase_receipt(item_code="Macbook Pro",
qty=2, rate=200000.0, location="Test Location") qty=2, rate=200000.0, location="Test Location")
@ -1197,7 +1248,8 @@ def create_asset(**args):
"available_for_use_date": args.available_for_use_date or "2020-06-06", "available_for_use_date": args.available_for_use_date or "2020-06-06",
"location": args.location or "Test Location", "location": args.location or "Test Location",
"asset_owner": args.asset_owner or "Company", "asset_owner": args.asset_owner or "Company",
"is_existing_asset": args.is_existing_asset or 1 "is_existing_asset": args.is_existing_asset or 1,
"asset_quantity": args.get("asset_quantity") or 1
}) })
if asset.calculate_depreciation: if asset.calculate_depreciation:

View File

@ -167,9 +167,14 @@ class AccountsController(TransactionBase):
validate_regional(self) validate_regional(self)
validate_einvoice_fields(self)
if self.doctype != 'Material Request': if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self) apply_pricing_rule_on_transaction(self)
def before_cancel(self):
validate_einvoice_fields(self)
def on_trash(self): def on_trash(self):
# delete sl and gl entries on deletion of transaction # delete sl and gl entries on deletion of transaction
if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'): if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
@ -2159,3 +2164,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
@erpnext.allow_regional @erpnext.allow_regional
def validate_regional(doc): def validate_regional(doc):
pass pass
@erpnext.allow_regional
def validate_einvoice_fields(doc):
pass

View File

@ -132,13 +132,17 @@ class EmployeeBoardingController(Document):
def on_cancel(self): def on_cancel(self):
# delete task project # delete task project
for task in frappe.get_all('Task', filters={'project': self.project}): project = self.project
for task in frappe.get_all('Task', filters={'project': project}):
frappe.delete_doc('Task', task.name, force=1) frappe.delete_doc('Task', task.name, force=1)
frappe.delete_doc('Project', self.project, force=1) frappe.delete_doc('Project', project, force=1)
self.db_set('project', '') self.db_set('project', '')
for activity in self.activities: for activity in self.activities:
activity.db_set('task', '') activity.db_set('task', '')
frappe.msgprint(_('Linked Project {} and Tasks deleted.').format(
project), alert=True, indicator='blue')
@frappe.whitelist() @frappe.whitelist()
def get_onboarding_details(parent, parenttype): def get_onboarding_details(parent, parenttype):

View File

@ -204,7 +204,7 @@ class SellingController(StockController):
valuation_rate_map = {} valuation_rate_map = {}
for item in self.items: for item in self.items:
if not item.item_code: if not item.item_code or item.is_free_item:
continue continue
last_purchase_rate, is_stock_item = frappe.get_cached_value( last_purchase_rate, is_stock_item = frappe.get_cached_value(
@ -251,7 +251,7 @@ class SellingController(StockController):
valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate valuation_rate_map[(rate.item_code, rate.warehouse)] = rate.valuation_rate
for item in self.items: for item in self.items:
if not item.item_code: if not item.item_code or item.is_free_item:
continue continue
last_valuation_rate = valuation_rate_map.get( last_valuation_rate = valuation_rate_map.get(

View File

@ -40,7 +40,10 @@ class StockController(AccountsController):
if self.docstatus == 2: if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if cint(erpnext.is_perpetual_inventory_enabled(self.company)): provisional_accounting_for_non_stock_items = \
cint(frappe.db.get_value('Company', self.company, 'enable_provisional_accounting_for_non_stock_items'))
if cint(erpnext.is_perpetual_inventory_enabled(self.company)) or provisional_accounting_for_non_stock_items:
warehouse_account = get_warehouse_account_map(self.company) warehouse_account = get_warehouse_account_map(self.company)
if self.docstatus==1: if self.docstatus==1:
@ -77,17 +80,17 @@ class StockController(AccountsController):
.format(d.idx, get_link_to_form("Batch", d.get("batch_no")))) .format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
def clean_serial_nos(self): def clean_serial_nos(self):
from erpnext.stock.doctype.serial_no.serial_no import clean_serial_no_string
for row in self.get("items"): for row in self.get("items"):
if hasattr(row, "serial_no") and row.serial_no: if hasattr(row, "serial_no") and row.serial_no:
# replace commas by linefeed # remove extra whitespace and store one serial no on each line
row.serial_no = row.serial_no.replace(",", "\n") row.serial_no = clean_serial_no_string(row.serial_no)
# strip preceeding and succeeding spaces for each SN for row in self.get('packed_items') or []:
# (SN could have valid spaces in between e.g. SN - 123 - 2021) if hasattr(row, "serial_no") and row.serial_no:
serial_no_list = row.serial_no.split("\n") # remove extra whitespace and store one serial no on each line
serial_no_list = [sn.strip() for sn in serial_no_list] row.serial_no = clean_serial_no_string(row.serial_no)
row.serial_no = "\n".join(serial_no_list)
def get_gl_entries(self, warehouse_account=None, default_expense_account=None, def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None): default_cost_center=None):

View File

@ -5,7 +5,7 @@ frappe.ui.form.on('Campaign', {
refresh: function(frm) { refresh: function(frm) {
erpnext.toggle_naming_series(); erpnext.toggle_naming_series();
if (frm.doc.__islocal) { if (frm.is_new()) {
frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series"); frm.toggle_display("naming_series", frappe.boot.sysdefaults.campaign_naming_by=="Naming Series");
} else { } else {
cur_frm.add_custom_button(__("View Leads"), function() { cur_frm.add_custom_button(__("View Leads"), function() {

View File

@ -1,9 +1,10 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class CRMSettings(Document): class CRMSettings(Document):
pass def validate(self):
frappe.db.set_default("campaign_naming_by", self.get("campaign_naming_by", ""))

View File

@ -24,6 +24,14 @@ frappe.ui.form.on("Opportunity", {
frm.trigger('set_contact_link'); frm.trigger('set_contact_link');
} }
}, },
validate: function(frm) {
if (frm.doc.status == "Lost" && !frm.doc.lost_reasons.length) {
frm.trigger('set_as_lost_dialog');
frappe.throw(__("Lost Reasons are required in case opportunity is Lost."));
}
},
contact_date: function(frm) { contact_date: function(frm) {
if(frm.doc.contact_date < frappe.datetime.now_datetime()){ if(frm.doc.contact_date < frappe.datetime.now_datetime()){
frm.set_value("contact_date", ""); frm.set_value("contact_date", "");
@ -82,7 +90,7 @@ frappe.ui.form.on("Opportunity", {
frm.trigger('setup_opportunity_from'); frm.trigger('setup_opportunity_from');
erpnext.toggle_naming_series(); erpnext.toggle_naming_series();
if(!doc.__islocal && doc.status!=="Lost") { if(!frm.is_new() && doc.status!=="Lost") {
if(doc.with_items){ if(doc.with_items){
frm.add_custom_button(__('Supplier Quotation'), frm.add_custom_button(__('Supplier Quotation'),
function() { function() {
@ -187,11 +195,11 @@ frappe.ui.form.on("Opportunity", {
change_form_labels: function(frm) { change_form_labels: function(frm) {
let company_currency = erpnext.get_currency(frm.doc.company); let company_currency = erpnext.get_currency(frm.doc.company);
frm.set_currency_labels(["base_opportunity_amount", "base_total", "base_grand_total"], company_currency); frm.set_currency_labels(["base_opportunity_amount", "base_total"], company_currency);
frm.set_currency_labels(["opportunity_amount", "total", "grand_total"], frm.doc.currency); frm.set_currency_labels(["opportunity_amount", "total"], frm.doc.currency);
// toggle fields // toggle fields
frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total", "base_grand_total"], frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total"],
frm.doc.currency != company_currency); frm.doc.currency != company_currency);
}, },
@ -209,20 +217,15 @@ frappe.ui.form.on("Opportunity", {
}, },
calculate_total: function(frm) { calculate_total: function(frm) {
let total = 0, base_total = 0, grand_total = 0, base_grand_total = 0; let total = 0, base_total = 0;
frm.doc.items.forEach(item => { frm.doc.items.forEach(item => {
total += item.amount; total += item.amount;
base_total += item.base_amount; base_total += item.base_amount;
}) })
base_grand_total = base_total + frm.doc.base_opportunity_amount;
grand_total = total + frm.doc.opportunity_amount;
frm.set_value({ frm.set_value({
'total': flt(total), 'total': flt(total),
'base_total': flt(base_total), 'base_total': flt(base_total)
'grand_total': flt(grand_total),
'base_grand_total': flt(base_grand_total)
}); });
} }

View File

@ -42,10 +42,8 @@
"items", "items",
"section_break_32", "section_break_32",
"base_total", "base_total",
"base_grand_total",
"column_break_33", "column_break_33",
"total", "total",
"grand_total",
"contact_info", "contact_info",
"customer_address", "customer_address",
"address_display", "address_display",
@ -475,21 +473,6 @@
"fieldname": "column_break_33", "fieldname": "column_break_33",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "base_grand_total",
"fieldtype": "Currency",
"label": "Grand Total (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "grand_total",
"fieldtype": "Currency",
"label": "Grand Total",
"options": "currency",
"read_only": 1
},
{ {
"fieldname": "lost_detail_section", "fieldname": "lost_detail_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -510,7 +493,7 @@
"icon": "fa fa-info-sign", "icon": "fa fa-info-sign",
"idx": 195, "idx": 195,
"links": [], "links": [],
"modified": "2021-10-21 12:04:30.151379", "modified": "2022-01-29 19:32:26.382896",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Opportunity", "name": "Opportunity",
@ -547,6 +530,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"subject_field": "title", "subject_field": "title",
"timeline_field": "party_name", "timeline_field": "party_name",
"title_field": "title", "title_field": "title",

View File

@ -69,8 +69,6 @@ class Opportunity(TransactionBase):
self.total = flt(total) self.total = flt(total)
self.base_total = flt(base_total) self.base_total = flt(base_total)
self.grand_total = flt(self.total) + flt(self.opportunity_amount)
self.base_grand_total = flt(self.base_total) + flt(self.base_opportunity_amount)
def make_new_lead_if_required(self): def make_new_lead_if_required(self):
"""Set lead against new opportunity""" """Set lead against new opportunity"""

View File

@ -3,6 +3,8 @@
frappe.ui.form.on('Prospect', { frappe.ui.form.on('Prospect', {
refresh (frm) { refresh (frm) {
frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: frm.doctype };
if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) {
frm.add_custom_button(__("Customer"), function() { frm.add_custom_button(__("Customer"), function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({

View File

@ -16,7 +16,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/CRM", "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/CRM",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"modified": "2020-07-08 14:05:42.644448", "modified": "2022-01-29 20:14:29.502145",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "CRM", "name": "CRM",
@ -33,6 +33,9 @@
}, },
{ {
"step": "Create and Send Quotation" "step": "Create and Send Quotation"
},
{
"step": "CRM Settings"
} }
], ],
"subtitle": "Lead, Opportunity, Customer, and more.", "subtitle": "Lead, Opportunity, Customer, and more.",

View File

@ -5,7 +5,6 @@
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-28 21:07:11.461172", "modified": "2020-05-28 21:07:11.461172",
@ -13,6 +12,7 @@
"name": "Create and Send Quotation", "name": "Create and Send Quotation",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Quotation", "reference_document": "Quotation",
"show_form_tour": 0,
"show_full_form": 1, "show_full_form": 1,
"title": "Create and Send Quotation", "title": "Create and Send Quotation",
"validate_action": 1 "validate_action": 1

View File

@ -5,7 +5,6 @@
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-28 21:07:01.373403", "modified": "2020-05-28 21:07:01.373403",
@ -13,6 +12,7 @@
"name": "Create Lead", "name": "Create Lead",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Lead", "reference_document": "Lead",
"show_form_tour": 0,
"show_full_form": 1, "show_full_form": 1,
"title": "Create Lead", "title": "Create Lead",
"validate_action": 1 "validate_action": 1

View File

@ -5,7 +5,6 @@
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2021-01-21 15:28:52.483839", "modified": "2021-01-21 15:28:52.483839",
@ -13,6 +12,7 @@
"name": "Create Opportunity", "name": "Create Opportunity",
"owner": "Administrator", "owner": "Administrator",
"reference_document": "Opportunity", "reference_document": "Opportunity",
"show_form_tour": 0,
"show_full_form": 1, "show_full_form": 1,
"title": "Create Opportunity", "title": "Create Opportunity",
"validate_action": 1 "validate_action": 1

View File

@ -0,0 +1,21 @@
{
"action": "Go to Page",
"creation": "2022-01-29 20:14:24.803844",
"description": "# CRM Settings\n\nCRM module\u2019s features are configurable as per your business needs. CRM Settings is the place where you can set your preferences for:\n- Campaign\n- Lead\n- Opportunity\n- Quotation",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 1,
"is_skipped": 0,
"modified": "2022-01-29 20:14:24.803844",
"modified_by": "Administrator",
"name": "CRM Settings",
"owner": "Administrator",
"path": "#crm-settings/CRM%20Settings",
"reference_document": "CRM Settings",
"show_form_tour": 0,
"show_full_form": 0,
"title": "CRM Settings",
"validate_action": 1
}

View File

@ -5,13 +5,13 @@
"doctype": "Onboarding Step", "doctype": "Onboarding Step",
"idx": 0, "idx": 0,
"is_complete": 0, "is_complete": 0,
"is_mandatory": 0,
"is_single": 0, "is_single": 0,
"is_skipped": 0, "is_skipped": 0,
"modified": "2020-05-14 17:28:16.448676", "modified": "2020-05-14 17:28:16.448676",
"modified_by": "Administrator", "modified_by": "Administrator",
"name": "Introduction to CRM", "name": "Introduction to CRM",
"owner": "Administrator", "owner": "Administrator",
"show_form_tour": 0,
"show_full_form": 0, "show_full_form": 0,
"title": "Introduction to CRM", "title": "Introduction to CRM",
"validate_action": 1, "validate_action": 1,

View File

@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.desk.reportview import get_match_cond from frappe.desk.reportview import get_match_cond
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.functions import Min
from frappe.utils import comma_and, get_link_to_form, getdate from frappe.utils import comma_and, get_link_to_form, getdate
@ -60,8 +61,15 @@ class ProgramEnrollment(Document):
frappe.throw(_("Student is already enrolled.")) frappe.throw(_("Student is already enrolled."))
def update_student_joining_date(self): def update_student_joining_date(self):
date = frappe.db.sql("select min(enrollment_date) from `tabProgram Enrollment` where student= %s", self.student) table = frappe.qb.DocType('Program Enrollment')
frappe.db.set_value("Student", self.student, "joining_date", date) date = (
frappe.qb.from_(table)
.select(Min(table.enrollment_date).as_('enrollment_date'))
.where(table.student == self.student)
).run(as_dict=True)
if date:
frappe.db.set_value("Student", self.student, "joining_date", date[0].enrollment_date)
def make_fee_records(self): def make_fee_records(self):
from erpnext.education.api import get_fee_components from erpnext.education.api import get_fee_components

View File

@ -443,6 +443,7 @@ regional_overrides = {
'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts', 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount', 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code' 'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
}, },

View File

@ -21,7 +21,7 @@ def get_data():
}, },
{ {
'label': _('Lifecycle'), 'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance'] 'items': ['Employee Onboarding', 'Employee Transfer', 'Employee Promotion', 'Employee Grievance']
}, },
{ {
'label': _('Exit'), 'label': _('Exit'),

View File

@ -20,6 +20,7 @@ def send_reminders_in_advance_weekly():
send_advance_holiday_reminders("Weekly") send_advance_holiday_reminders("Weekly")
def send_reminders_in_advance_monthly(): def send_reminders_in_advance_monthly():
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders")) to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
frequency = frappe.db.get_single_value("HR Settings", "frequency") frequency = frappe.db.get_single_value("HR Settings", "frequency")
@ -28,6 +29,7 @@ def send_reminders_in_advance_monthly():
send_advance_holiday_reminders("Monthly") send_advance_holiday_reminders("Monthly")
def send_advance_holiday_reminders(frequency): def send_advance_holiday_reminders(frequency):
"""Send Holiday Reminders in Advance to Employees """Send Holiday Reminders in Advance to Employees
`frequency` (str): 'Weekly' or 'Monthly' `frequency` (str): 'Weekly' or 'Monthly'
@ -42,7 +44,7 @@ def send_advance_holiday_reminders(frequency):
else: else:
return return
employees = frappe.db.get_all('Employee', pluck='name') employees = frappe.db.get_all('Employee', filters={'status': 'Active'}, pluck='name')
for employee in employees: for employee in employees:
holidays = get_holidays_for_employee( holidays = get_holidays_for_employee(
employee, employee,
@ -51,10 +53,13 @@ def send_advance_holiday_reminders(frequency):
raise_exception=False raise_exception=False
) )
if not (holidays is None): send_holidays_reminder_in_advance(employee, holidays)
send_holidays_reminder_in_advance(employee, holidays)
def send_holidays_reminder_in_advance(employee, holidays): def send_holidays_reminder_in_advance(employee, holidays):
if not holidays:
return
employee_doc = frappe.get_doc('Employee', employee) employee_doc = frappe.get_doc('Employee', employee)
employee_email = get_employee_email(employee_doc) employee_email = get_employee_email(employee_doc)
frequency = frappe.db.get_single_value("HR Settings", "frequency") frequency = frappe.db.get_single_value("HR Settings", "frequency")
@ -101,6 +106,7 @@ def send_birthday_reminders():
reminder_text, message = get_birthday_reminder_text_and_message(others) reminder_text, message = get_birthday_reminder_text_and_message(others)
send_birthday_reminder(person_email, reminder_text, others, message) send_birthday_reminder(person_email, reminder_text, others, message)
def get_birthday_reminder_text_and_message(birthday_persons): def get_birthday_reminder_text_and_message(birthday_persons):
if len(birthday_persons) == 1: if len(birthday_persons) == 1:
birthday_person_text = birthday_persons[0]['name'] birthday_person_text = birthday_persons[0]['name']
@ -116,6 +122,7 @@ def get_birthday_reminder_text_and_message(birthday_persons):
return reminder_text, message return reminder_text, message
def send_birthday_reminder(recipients, reminder_text, birthday_persons, message): def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
frappe.sendmail( frappe.sendmail(
recipients=recipients, recipients=recipients,
@ -129,10 +136,12 @@ def send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
header=_("Birthday Reminder 🎂") header=_("Birthday Reminder 🎂")
) )
def get_employees_who_are_born_today(): def get_employees_who_are_born_today():
"""Get all employee born today & group them based on their company""" """Get all employee born today & group them based on their company"""
return get_employees_having_an_event_today("birthday") return get_employees_having_an_event_today("birthday")
def get_employees_having_an_event_today(event_type): def get_employees_having_an_event_today(event_type):
"""Get all employee who have `event_type` today """Get all employee who have `event_type` today
& group them based on their company. `event_type` & group them based on their company. `event_type`
@ -210,13 +219,14 @@ def send_work_anniversary_reminders():
reminder_text, message = get_work_anniversary_reminder_text_and_message(others) reminder_text, message = get_work_anniversary_reminder_text_and_message(others)
send_work_anniversary_reminder(person_email, reminder_text, others, message) send_work_anniversary_reminder(person_email, reminder_text, others, message)
def get_work_anniversary_reminder_text_and_message(anniversary_persons): def get_work_anniversary_reminder_text_and_message(anniversary_persons):
if len(anniversary_persons) == 1: if len(anniversary_persons) == 1:
anniversary_person = anniversary_persons[0]['name'] anniversary_person = anniversary_persons[0]['name']
persons_name = anniversary_person persons_name = anniversary_person
# Number of years completed at the company # Number of years completed at the company
completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year completed_years = getdate().year - anniversary_persons[0]['date_of_joining'].year
anniversary_person += f" completed {completed_years} years" anniversary_person += f" completed {completed_years} year(s)"
else: else:
person_names_with_years = [] person_names_with_years = []
names = [] names = []
@ -225,7 +235,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
names.append(person_text) names.append(person_text)
# Number of years completed at the company # Number of years completed at the company
completed_years = getdate().year - person['date_of_joining'].year completed_years = getdate().year - person['date_of_joining'].year
person_text += f" completed {completed_years} years" person_text += f" completed {completed_years} year(s)"
person_names_with_years.append(person_text) person_names_with_years.append(person_text)
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
@ -239,6 +249,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
return reminder_text, message return reminder_text, message
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message): def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
frappe.sendmail( frappe.sendmail(
recipients=recipients, recipients=recipients,
@ -249,5 +260,5 @@ def send_work_anniversary_reminder(recipients, reminder_text, anniversary_person
anniversary_persons=anniversary_persons, anniversary_persons=anniversary_persons,
message=message, message=message,
), ),
header=_("🎊️🎊️ Work Anniversary Reminder 🎊️🎊️") header=_("Work Anniversary Reminder")
) )

View File

@ -36,7 +36,7 @@ class TestEmployee(unittest.TestCase):
employee_doc.reload() employee_doc.reload()
make_holiday_list() make_holiday_list()
frappe.db.set_value("Company", erpnext.get_default_company(), "default_holiday_list", "Salary Slip Test Holiday List") frappe.db.set_value("Company", employee_doc.company, "default_holiday_list", "Salary Slip Test Holiday List")
frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""") frappe.db.sql("""delete from `tabSalary Structure` where name='Test Inactive Employee Salary Slip'""")
salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly", salary_structure = make_salary_structure("Test Inactive Employee Salary Slip", "Monthly",

View File

@ -5,10 +5,12 @@ import unittest
from datetime import timedelta from datetime import timedelta
import frappe import frappe
from frappe.utils import getdate from frappe.utils import add_months, getdate
from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance
from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change from erpnext.hr.doctype.hr_settings.hr_settings import set_proceed_with_frequency_change
from erpnext.hr.utils import get_holidays_for_employee
class TestEmployeeReminders(unittest.TestCase): class TestEmployeeReminders(unittest.TestCase):
@ -46,6 +48,24 @@ class TestEmployeeReminders(unittest.TestCase):
cls.test_employee = test_employee cls.test_employee = test_employee
cls.test_holiday_dates = test_holiday_dates cls.test_holiday_dates = test_holiday_dates
# Employee without holidays in this month/week
test_employee_2 = make_employee('test@empwithoutholiday.io', company="_Test Company")
test_employee_2 = frappe.get_doc('Employee', test_employee_2)
test_holiday_list = make_holiday_list(
'TestHolidayRemindersList2',
holiday_dates=[
{'holiday_date': add_months(getdate(), 1), 'description': 'test holiday1'},
],
from_date=add_months(getdate(), -2),
to_date=add_months(getdate(), 2)
)
test_employee_2.holiday_list = test_holiday_list.name
test_employee_2.save()
cls.test_employee_2 = test_employee_2
cls.holiday_list_2 = test_holiday_list
@classmethod @classmethod
def get_test_holiday_dates(cls): def get_test_holiday_dates(cls):
today_date = getdate() today_date = getdate()
@ -61,6 +81,7 @@ class TestEmployeeReminders(unittest.TestCase):
def setUp(self): def setUp(self):
# Clear Email Queue # Clear Email Queue
frappe.db.sql("delete from `tabEmail Queue`") frappe.db.sql("delete from `tabEmail Queue`")
frappe.db.sql("delete from `tabEmail Queue Recipient`")
def test_is_holiday(self): def test_is_holiday(self):
from erpnext.hr.doctype.employee.employee import is_holiday from erpnext.hr.doctype.employee.employee import is_holiday
@ -103,11 +124,10 @@ class TestEmployeeReminders(unittest.TestCase):
self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message) self.assertTrue("Subject: Birthday Reminder" in email_queue[0].message)
def test_work_anniversary_reminders(self): def test_work_anniversary_reminders(self):
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0]) make_employee("test_work_anniversary@gmail.com",
employee.date_of_joining = "1998" + frappe.utils.nowdate()[4:] date_of_joining="1998" + frappe.utils.nowdate()[4:],
employee.company_email = "test@example.com" company="_Test Company",
employee.company = "_Test Company" )
employee.save()
from erpnext.hr.doctype.employee.employee_reminders import ( from erpnext.hr.doctype.employee.employee_reminders import (
get_employees_having_an_event_today, get_employees_having_an_event_today,
@ -115,7 +135,12 @@ class TestEmployeeReminders(unittest.TestCase):
) )
employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary') employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
self.assertTrue(employees_having_work_anniversary.get("_Test Company")) employees = employees_having_work_anniversary.get("_Test Company") or []
user_ids = []
for entry in employees:
user_ids.append(entry.user_id)
self.assertTrue("test_work_anniversary@gmail.com" in user_ids)
hr_settings = frappe.get_doc("HR Settings", "HR Settings") hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_work_anniversary_reminders = 1 hr_settings.send_work_anniversary_reminders = 1
@ -126,16 +151,24 @@ class TestEmployeeReminders(unittest.TestCase):
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message) self.assertTrue("Subject: Work Anniversary Reminder" in email_queue[0].message)
def test_send_holidays_reminder_in_advance(self): def test_work_anniversary_reminder_not_sent_for_0_years(self):
from erpnext.hr.doctype.employee.employee_reminders import send_holidays_reminder_in_advance make_employee("test_work_anniversary_2@gmail.com",
from erpnext.hr.utils import get_holidays_for_employee date_of_joining=getdate(),
company="_Test Company",
)
# Get HR settings and enable advance holiday reminders from erpnext.hr.doctype.employee.employee_reminders import get_employees_having_an_event_today
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_holiday_reminders = 1 employees_having_work_anniversary = get_employees_having_an_event_today('work_anniversary')
set_proceed_with_frequency_change() employees = employees_having_work_anniversary.get("_Test Company") or []
hr_settings.frequency = 'Weekly' user_ids = []
hr_settings.save() for entry in employees:
user_ids.append(entry.user_id)
self.assertTrue("test_work_anniversary_2@gmail.com" not in user_ids)
def test_send_holidays_reminder_in_advance(self):
setup_hr_settings('Weekly')
holidays = get_holidays_for_employee( holidays = get_holidays_for_employee(
self.test_employee.get('name'), self.test_employee.get('name'),
@ -151,32 +184,80 @@ class TestEmployeeReminders(unittest.TestCase):
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertEqual(len(email_queue), 1) self.assertEqual(len(email_queue), 1)
self.assertTrue("Holidays this Week." in email_queue[0].message)
def test_advance_holiday_reminders_monthly(self): def test_advance_holiday_reminders_monthly(self):
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_monthly
# Get HR settings and enable advance holiday reminders setup_hr_settings('Monthly')
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_holiday_reminders = 1 # disable emp 2, set same holiday list
set_proceed_with_frequency_change() frappe.db.set_value('Employee', self.test_employee_2.name, {
hr_settings.frequency = 'Monthly' 'status': 'Left',
hr_settings.save() 'holiday_list': self.test_employee.holiday_list
})
send_reminders_in_advance_monthly() send_reminders_in_advance_monthly()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue(len(email_queue) > 0) self.assertTrue(len(email_queue) > 0)
# even though emp 2 has holiday, non-active employees should not be recipients
recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient')
self.assertTrue(self.test_employee_2.user_id not in recipients)
# teardown: enable emp 2
frappe.db.set_value('Employee', self.test_employee_2.name, {
'status': 'Active',
'holiday_list': self.holiday_list_2.name
})
def test_advance_holiday_reminders_weekly(self): def test_advance_holiday_reminders_weekly(self):
from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly from erpnext.hr.doctype.employee.employee_reminders import send_reminders_in_advance_weekly
# Get HR settings and enable advance holiday reminders setup_hr_settings('Weekly')
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_holiday_reminders = 1 # disable emp 2, set same holiday list
hr_settings.frequency = 'Weekly' frappe.db.set_value('Employee', self.test_employee_2.name, {
hr_settings.save() 'status': 'Left',
'holiday_list': self.test_employee.holiday_list
})
send_reminders_in_advance_weekly() send_reminders_in_advance_weekly()
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue(len(email_queue) > 0) self.assertTrue(len(email_queue) > 0)
# even though emp 2 has holiday, non-active employees should not be recipients
recipients = frappe.db.get_all('Email Queue Recipient', pluck='recipient')
self.assertTrue(self.test_employee_2.user_id not in recipients)
# teardown: enable emp 2
frappe.db.set_value('Employee', self.test_employee_2.name, {
'status': 'Active',
'holiday_list': self.holiday_list_2.name
})
def test_reminder_not_sent_if_no_holdays(self):
setup_hr_settings('Monthly')
# reminder not sent if there are no holidays
holidays = get_holidays_for_employee(
self.test_employee_2.get('name'),
getdate(), getdate() + timedelta(days=3),
only_non_weekly=True,
raise_exception=False
)
send_holidays_reminder_in_advance(
self.test_employee_2.get('name'),
holidays
)
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertEqual(len(email_queue), 0)
def setup_hr_settings(frequency=None):
# Get HR settings and enable advance holiday reminders
hr_settings = frappe.get_doc("HR Settings", "HR Settings")
hr_settings.send_holiday_reminders = 1
set_proceed_with_frequency_change()
hr_settings.frequency = frequency or 'Weekly'
hr_settings.save()

View File

@ -62,6 +62,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:['Employee Onboarding', 'Employee Onboarding Template'].includes(doc.parenttype)",
"description": "Applicable in the case of Employee Onboarding", "description": "Applicable in the case of Employee Onboarding",
"fieldname": "required_for_employee_creation", "fieldname": "required_for_employee_creation",
"fieldtype": "Check", "fieldtype": "Check",
@ -93,7 +94,7 @@
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-07-30 15:55:22.470102", "modified": "2022-01-29 14:05:00.543122",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Boarding Activity", "name": "Employee Boarding Activity",
@ -102,5 +103,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -3,12 +3,6 @@
frappe.ui.form.on('Employee Onboarding', { frappe.ui.form.on('Employee Onboarding', {
setup: function(frm) { setup: function(frm) {
frm.add_fetch("employee_onboarding_template", "company", "company");
frm.add_fetch("employee_onboarding_template", "department", "department");
frm.add_fetch("employee_onboarding_template", "designation", "designation");
frm.add_fetch("employee_onboarding_template", "employee_grade", "employee_grade");
frm.set_query("job_applicant", function () { frm.set_query("job_applicant", function () {
return { return {
filters:{ filters:{
@ -71,5 +65,19 @@ frappe.ui.form.on('Employee Onboarding', {
} }
}); });
} }
},
job_applicant: function(frm) {
if (frm.doc.job_applicant) {
frappe.db.get_value('Employee', {'job_applicant': frm.doc.job_applicant}, 'name', (r) => {
if (r.name) {
frm.set_value('employee', r.name);
} else {
frm.set_value('employee', '');
}
});
} else {
frm.set_value('employee', '');
}
} }
}); });

View File

@ -92,6 +92,7 @@
"options": "Employee Onboarding Template" "options": "Employee Onboarding Template"
}, },
{ {
"fetch_from": "employee_onboarding_template.company",
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
@ -99,6 +100,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"fetch_from": "employee_onboarding_template.department",
"fieldname": "department", "fieldname": "department",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -106,6 +108,7 @@
"options": "Department" "options": "Department"
}, },
{ {
"fetch_from": "employee_onboarding_template.designation",
"fieldname": "designation", "fieldname": "designation",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
@ -113,6 +116,7 @@
"options": "Designation" "options": "Designation"
}, },
{ {
"fetch_from": "employee_onboarding_template.employee_grade",
"fieldname": "employee_grade", "fieldname": "employee_grade",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Employee Grade", "label": "Employee Grade",
@ -170,10 +174,11 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-07-30 14:55:04.560683", "modified": "2022-01-29 12:33:57.120384",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Onboarding", "name": "Employee Onboarding",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -194,6 +199,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "employee_name", "title_field": "employee_name",
"track_changes": 1 "track_changes": 1
} }

View File

@ -14,10 +14,15 @@ class IncompleteTaskError(frappe.ValidationError): pass
class EmployeeOnboarding(EmployeeBoardingController): class EmployeeOnboarding(EmployeeBoardingController):
def validate(self): def validate(self):
super(EmployeeOnboarding, self).validate() super(EmployeeOnboarding, self).validate()
self.set_employee()
self.validate_duplicate_employee_onboarding() self.validate_duplicate_employee_onboarding()
def set_employee(self):
if not self.employee:
self.employee = frappe.db.get_value('Employee', {'job_applicant': self.job_applicant}, 'name')
def validate_duplicate_employee_onboarding(self): def validate_duplicate_employee_onboarding(self):
emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant}) emp_onboarding = frappe.db.exists("Employee Onboarding", {"job_applicant": self.job_applicant, "docstatus": ("!=", 2)})
if emp_onboarding and emp_onboarding != self.name: if emp_onboarding and emp_onboarding != self.name:
frappe.throw(_("Employee Onboarding: {0} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant))) frappe.throw(_("Employee Onboarding: {0} already exists for Job Applicant: {1}").format(frappe.bold(emp_onboarding), frappe.bold(self.job_applicant)))

View File

@ -7,7 +7,7 @@ import datetime
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cstr, get_datetime, get_link_to_form from frappe.utils import cstr, flt, get_datetime, get_link_to_form
class DuplicateInterviewRoundError(frappe.ValidationError): class DuplicateInterviewRoundError(frappe.ValidationError):
@ -18,6 +18,7 @@ class Interview(Document):
self.validate_duplicate_interview() self.validate_duplicate_interview()
self.validate_designation() self.validate_designation()
self.validate_overlap() self.validate_overlap()
self.set_average_rating()
def on_submit(self): def on_submit(self):
if self.status not in ['Cleared', 'Rejected']: if self.status not in ['Cleared', 'Rejected']:
@ -67,6 +68,13 @@ class Interview(Document):
overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0])) overlapping_details = _('Interview overlaps with {0}').format(get_link_to_form('Interview', overlaps[0][0]))
frappe.throw(overlapping_details, title=_('Overlap')) frappe.throw(overlapping_details, title=_('Overlap'))
def set_average_rating(self):
total_rating = 0
for entry in self.interview_details:
if entry.average_rating:
total_rating += entry.average_rating
self.average_rating = flt(total_rating / len(self.interview_details) if len(self.interview_details) else 0)
@frappe.whitelist() @frappe.whitelist()
def reschedule_interview(self, scheduled_on, from_time, to_time): def reschedule_interview(self, scheduled_on, from_time, to_time):

View File

@ -12,6 +12,7 @@ from frappe.utils import add_days, getdate, nowtime
from erpnext.hr.doctype.designation.test_designation import create_designation from erpnext.hr.doctype.designation.test_designation import create_designation
from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError from erpnext.hr.doctype.interview.interview import DuplicateInterviewRoundError
from erpnext.hr.doctype.job_applicant.job_applicant import get_interview_details
from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant from erpnext.hr.doctype.job_applicant.test_job_applicant import create_job_applicant
@ -70,6 +71,20 @@ class TestInterview(unittest.TestCase):
email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True) email_queue = frappe.db.sql("""select * from `tabEmail Queue`""", as_dict=True)
self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message) self.assertTrue("Subject: Interview Feedback Reminder" in email_queue[0].message)
def test_get_interview_details_for_applicant_dashboard(self):
job_applicant = create_job_applicant()
interview = create_interview_and_dependencies(job_applicant.name)
details = get_interview_details(job_applicant.name)
self.assertEqual(details.get('stars'), 5)
self.assertEqual(details.get('interviews').get(interview.name), {
'name': interview.name,
'interview_round': interview.interview_round,
'expected_average_rating': interview.expected_average_rating * 5,
'average_rating': interview.average_rating * 5,
'status': 'Pending'
})
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
@ -106,7 +121,8 @@ def create_interview_round(name, skill_set, interviewers=[], designation=None, s
interview_round = frappe.new_doc("Interview Round") interview_round = frappe.new_doc("Interview Round")
interview_round.round_name = name interview_round.round_name = name
interview_round.interview_type = create_interview_type() interview_round.interview_type = create_interview_type()
interview_round.expected_average_rating = 4 # average rating = 4
interview_round.expected_average_rating = 0.8
if designation: if designation:
interview_round.designation = designation interview_round.designation = designation

View File

@ -57,7 +57,6 @@ class InterviewFeedback(Document):
def update_interview_details(self): def update_interview_details(self):
doc = frappe.get_doc('Interview', self.interview) doc = frappe.get_doc('Interview', self.interview)
total_rating = 0
if self.docstatus == 2: if self.docstatus == 2:
for entry in doc.interview_details: for entry in doc.interview_details:
@ -72,10 +71,6 @@ class InterviewFeedback(Document):
entry.comments = self.feedback entry.comments = self.feedback
entry.result = self.result entry.result = self.result
if entry.average_rating:
total_rating += entry.average_rating
doc.average_rating = flt(total_rating / len(doc.interview_details) if len(doc.interview_details) else 0)
doc.save() doc.save()
doc.notify_update() doc.notify_update()

View File

@ -24,7 +24,7 @@ class TestInterviewFeedback(unittest.TestCase):
create_skill_set(['Leadership']) create_skill_set(['Leadership'])
interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings) interview_feedback = create_interview_feedback(interview.name, interviewer, skill_ratings)
interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 4}) interview_feedback.append("skill_assessment", {"skill": 'Leadership', 'rating': 0.8})
frappe.set_user(interviewer) frappe.set_user(interviewer)
self.assertRaises(frappe.ValidationError, interview_feedback.save) self.assertRaises(frappe.ValidationError, interview_feedback.save)
@ -50,7 +50,7 @@ class TestInterviewFeedback(unittest.TestCase):
avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0) avg_rating = flt(total_rating / len(feedback_1.skill_assessment) if len(feedback_1.skill_assessment) else 0)
self.assertEqual(flt(avg_rating, 3), feedback_1.average_rating) self.assertEqual(flt(avg_rating, 2), flt(feedback_1.average_rating, 2))
avg_on_interview_detail = frappe.db.get_value('Interview Detail', { avg_on_interview_detail = frappe.db.get_value('Interview Detail', {
'parent': feedback_1.interview, 'parent': feedback_1.interview,
@ -59,7 +59,7 @@ class TestInterviewFeedback(unittest.TestCase):
}, 'average_rating') }, 'average_rating')
# 1. average should be reflected in Interview Detail. # 1. average should be reflected in Interview Detail.
self.assertEqual(avg_on_interview_detail, feedback_1.average_rating) self.assertEqual(flt(avg_on_interview_detail, 2), flt(feedback_1.average_rating, 2))
'''For Second Interviewer Feedback''' '''For Second Interviewer Feedback'''
interviewer = interview.interview_details[1].interviewer interviewer = interview.interview_details[1].interviewer
@ -97,5 +97,5 @@ def get_skills_rating(interview_round):
skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"]) skills = frappe.get_all("Expected Skill Set", filters={"parent": interview_round}, fields = ["skill"])
for d in skills: for d in skills:
d["rating"] = random.randint(1, 5) d["rating"] = random.random()
return skills return skills

View File

@ -21,9 +21,9 @@ frappe.ui.form.on("Job Applicant", {
create_custom_buttons: function(frm) { create_custom_buttons: function(frm) {
if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") { if (!frm.doc.__islocal && frm.doc.status !== "Rejected" && frm.doc.status !== "Accepted") {
frm.add_custom_button(__("Create Interview"), function() { frm.add_custom_button(__("Interview"), function() {
frm.events.create_dialog(frm); frm.events.create_dialog(frm);
}); }, __("Create"));
} }
if (!frm.doc.__islocal) { if (!frm.doc.__islocal) {
@ -40,10 +40,10 @@ frappe.ui.form.on("Job Applicant", {
frappe.route_options = { frappe.route_options = {
"job_applicant": frm.doc.name, "job_applicant": frm.doc.name,
"applicant_name": frm.doc.applicant_name, "applicant_name": frm.doc.applicant_name,
"designation": frm.doc.job_opening, "designation": frm.doc.job_opening || frm.doc.designation,
}; };
frappe.new_doc("Job Offer"); frappe.new_doc("Job Offer");
}); }, __("Create"));
} }
} }
}, },
@ -55,13 +55,16 @@ frappe.ui.form.on("Job Applicant", {
job_applicant: frm.doc.name job_applicant: frm.doc.name
}, },
callback: function(r) { callback: function(r) {
$("div").remove(".form-dashboard-section.custom"); if (r.message) {
frm.dashboard.add_section( $("div").remove(".form-dashboard-section.custom");
frappe.render_template('job_applicant_dashboard', { frm.dashboard.add_section(
data: r.message frappe.render_template("job_applicant_dashboard", {
}), data: r.message.interviews,
__("Interview Summary") number_of_stars: r.message.stars
); }),
__("Interview Summary")
);
}
} }
}); });
}, },

View File

@ -81,8 +81,13 @@ def get_interview_details(job_applicant):
fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"] fields=["name", "interview_round", "expected_average_rating", "average_rating", "status"]
) )
interview_detail_map = {} interview_detail_map = {}
meta = frappe.get_meta("Interview")
number_of_stars = meta.get_options("expected_average_rating") or 5
for detail in interview_details: for detail in interview_details:
detail.expected_average_rating = detail.expected_average_rating * number_of_stars if detail.expected_average_rating else 0
detail.average_rating = detail.average_rating * number_of_stars if detail.average_rating else 0
interview_detail_map[detail.name] = detail interview_detail_map[detail.name] = detail
return interview_detail_map return {"interviews": interview_detail_map, "stars": number_of_stars}

View File

@ -17,24 +17,33 @@
<td class="text-left"> {%= key %} </td> <td class="text-left"> {%= key %} </td>
<td class="text-left"> {%= value["interview_round"] %} </td> <td class="text-left"> {%= value["interview_round"] %} </td>
<td class="text-left"> {%= value["status"] %} </td> <td class="text-left"> {%= value["status"] %} </td>
<td class="text-left"> {% let right_class = ''; %}
{% for (i = 0; i < value["expected_average_rating"]; i++) { %} {% let left_class = ''; %}
<span class="fa fa-star " style="color: #F6C35E;"></span>
{% } %} {% $.each([value["expected_average_rating"], value["average_rating"]], (_, val) => { %}
{% for (i = 0; i < (5-value["expected_average_rating"]); i++) { %} <td class="text-left">
<span class="fa fa-star " style="color: #E7E9EB;"></span> <div class="rating">
{% } %} {% for (let i = 1; i <= number_of_stars; i++) { %}
</td> {% if (i <= val) { %}
<td class="text-left"> {% right_class = 'star-click'; %}
{% if(value["average_rating"]){ %} {% } else { %}
{% for (i = 0; i < value["average_rating"]; i++) { %} {% right_class = ''; %}
<span class="fa fa-star " style="color: #F6C35E;"></span> {% } %}
{% } %}
{% for (i = 0; i < (5-value["average_rating"]); i++) { %} {% if ((i <= val) || ((i - 0.5) == val)) { %}
<span class="fa fa-star " style="color: #E7E9EB;"></span> {% left_class = 'star-click'; %}
{% } %} {% } else { %}
{% } %} {% left_class = ''; %}
</td> {% } %}
<svg class="icon icon-md" data-rating={{i}} viewBox="0 0 24 24" fill="none">
<path class="right-half {{ right_class }}" d="M11.9987 3.00011C12.177 3.00011 12.3554 3.09303 12.4471 3.27888L14.8213 8.09112C14.8941 8.23872 15.0349 8.34102 15.1978 8.3647L20.5069 9.13641C20.917 9.19602 21.0807 9.69992 20.7841 9.9892L16.9421 13.7354C16.8243 13.8503 16.7706 14.0157 16.7984 14.1779L17.7053 19.4674C17.7753 19.8759 17.3466 20.1874 16.9798 19.9945L12.2314 17.4973C12.1586 17.459 12.0786 17.4398 11.9987 17.4398V3.00011Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
<path class="left-half {{ left_class }}" d="M11.9987 3.00011C11.8207 3.00011 11.6428 3.09261 11.5509 3.27762L9.15562 8.09836C9.08253 8.24546 8.94185 8.34728 8.77927 8.37075L3.42887 9.14298C3.01771 9.20233 2.85405 9.70811 3.1525 9.99707L7.01978 13.7414C7.13858 13.8564 7.19283 14.0228 7.16469 14.1857L6.25116 19.4762C6.18071 19.8842 6.6083 20.1961 6.97531 20.0045L11.7672 17.5022C11.8397 17.4643 11.9192 17.4454 11.9987 17.4454V3.00011Z" fill="var(--star-fill)" stroke="var(--star-fill)"/>
</svg>
{% } %}
</div>
</td>
{% }); %}
</tr> </tr>
{% } %} {% } %}
</tbody> </tbody>

View File

@ -78,6 +78,7 @@ def make_employee(source_name, target_doc=None):
"doctype": "Employee", "doctype": "Employee",
"field_map": { "field_map": {
"applicant_name": "employee_name", "applicant_name": "employee_name",
"offer_date": "scheduled_confirmation_date"
}} }}
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
return doc return doc

View File

@ -75,10 +75,8 @@ class TestLeaveApplication(unittest.TestCase):
frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec frappe.db.sql("DELETE FROM `tab%s`" % dt) #nosec
frappe.set_user("Administrator") frappe.set_user("Administrator")
@classmethod
def setUpClass(cls):
set_leave_approver() set_leave_approver()
frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'") frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
def tearDown(self): def tearDown(self):
@ -134,10 +132,11 @@ class TestLeaveApplication(unittest.TestCase):
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
holiday_list = make_holiday_list() holiday_list = make_holiday_list()
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list) employee = get_employee()
frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list) first_sunday = get_first_sunday(holiday_list)
leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
leave_application.reload() leave_application.reload()
self.assertEqual(leave_application.total_leave_days, 4) self.assertEqual(leave_application.total_leave_days, 4)
self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4) self.assertEqual(frappe.db.count('Attendance', {'leave_application': leave_application.name}), 4)
@ -157,25 +156,28 @@ class TestLeaveApplication(unittest.TestCase):
make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date)) make_allocation_record(leave_type=leave_type.name, from_date=get_year_start(date), to_date=get_year_ending(date))
holiday_list = make_holiday_list() holiday_list = make_holiday_list()
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", holiday_list) employee = get_employee()
frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list) first_sunday = get_first_sunday(holiday_list)
# already marked attendance on a holiday should be deleted in this case # already marked attendance on a holiday should be deleted in this case
config = { config = {
"doctype": "Attendance", "doctype": "Attendance",
"employee": "_T-Employee-00001", "employee": employee.name,
"status": "Present" "status": "Present"
} }
attendance_on_holiday = frappe.get_doc(config) attendance_on_holiday = frappe.get_doc(config)
attendance_on_holiday.attendance_date = first_sunday attendance_on_holiday.attendance_date = first_sunday
attendance_on_holiday.flags.ignore_validate = True
attendance_on_holiday.save() attendance_on_holiday.save()
# already marked attendance on a non-holiday should be updated # already marked attendance on a non-holiday should be updated
attendance = frappe.get_doc(config) attendance = frappe.get_doc(config)
attendance.attendance_date = add_days(first_sunday, 3) attendance.attendance_date = add_days(first_sunday, 3)
attendance.flags.ignore_validate = True
attendance.save() attendance.save()
leave_application = make_leave_application("_T-Employee-00001", first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
leave_application.reload() leave_application.reload()
# holiday should be excluded while marking attendance # holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3) self.assertEqual(leave_application.total_leave_days, 3)
@ -325,7 +327,7 @@ class TestLeaveApplication(unittest.TestCase):
employee = get_employee() employee = get_employee()
default_holiday_list = make_holiday_list() default_holiday_list = make_holiday_list()
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", default_holiday_list) frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list)
first_sunday = get_first_sunday(default_holiday_list) first_sunday = get_first_sunday(default_holiday_list)
optional_leave_date = add_days(first_sunday, 1) optional_leave_date = add_days(first_sunday, 1)

View File

@ -70,7 +70,6 @@
{ {
"fieldname": "loan_repayment_entry", "fieldname": "loan_repayment_entry",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 1,
"label": "Loan Repayment Entry", "label": "Loan Repayment Entry",
"no_copy": 1, "no_copy": 1,
"options": "Loan Repayment", "options": "Loan Repayment",
@ -88,7 +87,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-03-14 20:47:11.725818", "modified": "2022-01-31 14:50:14.823213",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Loan Management", "module": "Loan Management",
"name": "Salary Slip Loan", "name": "Salary Slip Loan",
@ -97,5 +96,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -93,7 +93,7 @@ frappe.ui.form.on("BOM", {
}); });
} }
if(frm.doc.docstatus!=0) { if(frm.doc.docstatus==1) {
frm.add_custom_button(__("Work Order"), function() { frm.add_custom_button(__("Work Order"), function() {
frm.trigger("make_work_order"); frm.trigger("make_work_order");
}, __("Create")); }, __("Create"));

View File

@ -37,7 +37,6 @@
"inspection_required", "inspection_required",
"quality_inspection_template", "quality_inspection_template",
"column_break_31", "column_break_31",
"bom_level",
"section_break_33", "section_break_33",
"items", "items",
"scrap_section", "scrap_section",
@ -522,13 +521,6 @@
"fieldname": "column_break_31", "fieldname": "column_break_31",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"default": "0",
"fieldname": "bom_level",
"fieldtype": "Int",
"label": "BOM Level",
"read_only": 1
},
{ {
"fieldname": "section_break_33", "fieldname": "section_break_33",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -540,7 +532,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-11-18 13:04:16.271975", "modified": "2022-01-30 21:27:54.727298",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",
@ -577,5 +569,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",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -155,7 +155,6 @@ class BOM(WebsiteGenerator):
self.calculate_cost() self.calculate_cost()
self.update_stock_qty() self.update_stock_qty()
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False) self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate = False, save=False)
self.set_bom_level()
self.validate_scrap_items() self.validate_scrap_items()
def get_context(self, context): def get_context(self, context):
@ -716,20 +715,6 @@ class BOM(WebsiteGenerator):
"""Get a complete tree representation preserving order of child items.""" """Get a complete tree representation preserving order of child items."""
return BOMTree(self.name) return BOMTree(self.name)
def set_bom_level(self, update=False):
levels = []
self.bom_level = 0
for row in self.items:
if row.bom_no:
levels.append(frappe.get_cached_value("BOM", row.bom_no, "bom_level") or 0)
if levels:
self.bom_level = max(levels) + 1
if update:
self.db_set("bom_level", self.bom_level)
def validate_scrap_items(self): def validate_scrap_items(self):
for item in self.scrap_items: for item in self.scrap_items:
msg = "" msg = ""

View File

@ -559,9 +559,11 @@ class ProductionPlan(Document):
get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty)
self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type)
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True)
bom_data = sorted(bom_data, key = lambda i: i.bom_level) for idx, row in enumerate(self.sub_assembly_items, start=1):
row.idx = idx
def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None):
for data in bom_data: for data in bom_data:
data.qty = data.stock_qty data.qty = data.stock_qty
data.production_plan_item = row.name data.production_plan_item = row.name
@ -1004,9 +1006,6 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
for d in data: for d in data:
if d.expandable: if d.expandable:
parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") parent_item_code = frappe.get_cached_value("BOM", bom_no, "item")
bom_level = (frappe.get_cached_value("BOM", d.value, "bom_level")
if d.value else 0)
stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty) stock_qty = (d.stock_qty / d.parent_bom_qty) * flt(to_produce_qty)
bom_data.append(frappe._dict({ bom_data.append(frappe._dict({
'parent_item_code': parent_item_code, 'parent_item_code': parent_item_code,
@ -1017,7 +1016,7 @@ def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0):
'uom': d.stock_uom, 'uom': d.stock_uom,
'bom_no': d.value, 'bom_no': d.value,
'is_sub_contracted_item': d.is_sub_contracted_item, 'is_sub_contracted_item': d.is_sub_contracted_item,
'bom_level': bom_level, 'bom_level': indent,
'indent': indent, 'indent': indent,
'stock_qty': stock_qty 'stock_qty': stock_qty
})) }))

View File

@ -347,6 +347,45 @@ class TestProductionPlan(ERPNextTestCase):
frappe.db.rollback() frappe.db.rollback()
def test_subassmebly_sorting(self):
""" Test subassembly sorting in case of multiple items with nested BOMs"""
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom
prefix = "_TestLevel_"
boms = {
"Assembly": {
"SubAssembly1": {"ChildPart1": {}, "ChildPart2": {},},
"SubAssembly2": {"ChildPart3": {}},
"SubAssembly3": {"SubSubAssy1": {"ChildPart4": {}}},
"ChildPart5": {},
"ChildPart6": {},
"SubAssembly4": {"SubSubAssy2": {"ChildPart7": {}}},
},
"MegaDeepAssy": {
"SecretSubassy": {"SecretPart": {"VerySecret" : { "SuperSecret": {"Classified": {}}}},},
# ^ assert that this is
# first item in subassy table
}
}
create_nested_bom(boms, prefix=prefix)
items = [prefix + item_code for item_code in boms.keys()]
plan = create_production_plan(item_code=items[0], do_not_save=True)
plan.append("po_items", {
'use_multi_level_bom': 1,
'item_code': items[1],
'bom_no': frappe.db.get_value('Item', items[1], 'default_bom'),
'planned_qty': 1,
'planned_start_date': now_datetime()
})
plan.get_sub_assembly_items()
bom_level_order = [d.bom_level for d in plan.sub_assembly_items]
self.assertEqual(bom_level_order, sorted(bom_level_order, reverse=True))
# lowest most level of subassembly should be first
self.assertIn("SuperSecret", plan.sub_assembly_items[0].production_item)
def create_production_plan(**args): def create_production_plan(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -102,7 +102,6 @@
}, },
{ {
"columns": 1, "columns": 1,
"fetch_from": "bom_no.bom_level",
"fieldname": "bom_level", "fieldname": "bom_level",
"fieldtype": "Int", "fieldtype": "Int",
"in_list_view": 1, "in_list_view": 1,
@ -189,7 +188,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-06-28 20:10:56.296410", "modified": "2022-01-30 21:31:10.527559",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "Production Plan Sub Assembly Item", "name": "Production Plan Sub Assembly Item",
@ -198,5 +197,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -31,6 +31,7 @@ from erpnext.stock.doctype.batch.batch import make_batch
from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life from erpnext.stock.doctype.item.item import get_item_defaults, validate_end_of_life
from erpnext.stock.doctype.serial_no.serial_no import ( from erpnext.stock.doctype.serial_no.serial_no import (
auto_make_serial_nos, auto_make_serial_nos,
clean_serial_no_string,
get_auto_serial_nos, get_auto_serial_nos,
get_serial_nos, get_serial_nos,
) )
@ -358,6 +359,7 @@ class WorkOrder(Document):
frappe.delete_doc("Batch", row.name) frappe.delete_doc("Batch", row.name)
def make_serial_nos(self, args): def make_serial_nos(self, args):
self.serial_no = clean_serial_no_string(self.serial_no)
serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series") serial_no_series = frappe.get_cached_value("Item", self.production_item, "serial_no_series")
if serial_no_series: if serial_no_series:
self.serial_no = get_auto_serial_nos(serial_no_series, self.qty) self.serial_no = get_auto_serial_nos(serial_no_series, self.qty)

View File

@ -26,8 +26,7 @@ def get_exploded_items(bom, data, indent=0, qty=1):
'item_code': item.item_code, 'item_code': item.item_code,
'item_name': item.item_name, 'item_name': item.item_name,
'indent': indent, 'indent': indent,
'bom_level': (frappe.get_cached_value("BOM", item.bom_no, "bom_level") 'bom_level': indent,
if item.bom_no else ""),
'bom': item.bom_no, 'bom': item.bom_no,
'qty': item.qty * qty, 'qty': item.qty * qty,
'uom': item.uom, 'uom': item.uom,
@ -73,7 +72,7 @@ def get_columns():
}, },
{ {
"label": "BOM Level", "label": "BOM Level",
"fieldtype": "Data", "fieldtype": "Int",
"fieldname": "bom_level", "fieldname": "bom_level",
"width": 100 "width": 100
}, },

View File

@ -17,14 +17,12 @@ frappe.query_reports["Cost of Poor Quality Report"] = {
fieldname:"from_date", fieldname:"from_date",
fieldtype: "Datetime", fieldtype: "Datetime",
default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)), default: frappe.datetime.convert_to_system_tz(frappe.datetime.add_months(frappe.datetime.now_datetime(), -1)),
reqd: 1
}, },
{ {
label: __("To Date"), label: __("To Date"),
fieldname:"to_date", fieldname:"to_date",
fieldtype: "Datetime", fieldtype: "Datetime",
default: frappe.datetime.now_datetime(), default: frappe.datetime.now_datetime(),
reqd: 1,
}, },
{ {
label: __("Job Card"), label: __("Job Card"),

View File

@ -3,46 +3,65 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt
def execute(filters=None): def execute(filters=None):
columns, data = [], [] return get_columns(filters), get_data(filters)
columns = get_columns(filters)
data = get_data(filters)
return columns, data
def get_data(report_filters): def get_data(report_filters):
data = [] data = []
operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1}) operations = frappe.get_all("Operation", filters = {"is_corrective_operation": 1})
if operations: if operations:
operations = [d.name for d in operations] if report_filters.get('operation'):
fields = ["production_item as item_code", "item_name", "work_order", "operation", operations = [report_filters.get('operation')]
"workstation", "total_time_in_mins", "name", "hour_rate", "serial_no", "batch_no"] else:
operations = [d.name for d in operations]
filters = get_filters(report_filters, operations) job_card = frappe.qb.DocType("Job Card")
job_cards = frappe.get_all("Job Card", fields = fields, operating_cost = ((job_card.hour_rate) * (job_card.total_time_in_mins) / 60.0).as_('operating_cost')
filters = filters) item_code = (job_card.production_item).as_('item_code')
for row in job_cards: query = (frappe.qb
row.operating_cost = flt(row.hour_rate) * (flt(row.total_time_in_mins) / 60.0) .from_(job_card)
data.append(row) .select(job_card.name, job_card.work_order, item_code, job_card.item_name,
job_card.operation, job_card.serial_no, job_card.batch_no,
job_card.workstation, job_card.total_time_in_mins, job_card.hour_rate,
operating_cost)
.where(
(job_card.docstatus == 1)
& (job_card.is_corrective_job_card == 1))
.groupby(job_card.name)
)
query = append_filters(query, report_filters, operations, job_card)
data = query.run(as_dict=True)
return data return data
def get_filters(report_filters, operations): def append_filters(query, report_filters, operations, job_card):
filters = {"docstatus": 1, "operation": ("in", operations), "is_corrective_job_card": 1} """Append optional filters to query builder. """
for field in ["name", "work_order", "operation", "workstation", "company", "serial_no", "batch_no", "production_item"]:
if report_filters.get(field):
if field != 'serial_no':
filters[field] = report_filters.get(field)
else:
filters[field] = ('like', '% {} %'.format(report_filters.get(field)))
return filters for field in ("name", "work_order", "operation", "workstation",
"company", "serial_no", "batch_no", "production_item"):
if report_filters.get(field):
if field == 'serial_no':
query = query.where(job_card[field].like('%{}%'.format(report_filters.get(field))))
elif field == 'operation':
query = query.where(job_card[field].isin(operations))
else:
query = query.where(job_card[field] == report_filters.get(field))
if report_filters.get('from_date') or report_filters.get('to_date'):
job_card_time_log = frappe.qb.DocType("Job Card Time Log")
query = query.join(job_card_time_log).on(job_card.name == job_card_time_log.parent)
if report_filters.get('from_date'):
query = query.where(job_card_time_log.from_time >= report_filters.get('from_date'))
if report_filters.get('to_date'):
query = query.where(job_card_time_log.to_time <= report_filters.get('to_date'))
return query
def get_columns(filters): def get_columns(filters):
return [ return [

View File

@ -48,7 +48,7 @@ def get_production_plan_item_details(filters, data, order_details):
"qty": row.planned_qty, "qty": row.planned_qty,
"document_type": "Work Order", "document_type": "Work Order",
"document_name": work_order or "", "document_name": work_order or "",
"bom_level": frappe.get_cached_value("BOM", row.bom_no, "bom_level"), "bom_level": 0,
"produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0), "produced_qty": order_details.get((work_order, row.item_code), {}).get("produced_qty", 0),
"pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0)) "pending_qty": flt(row.planned_qty) - flt(order_details.get((work_order, row.item_code), {}).get("produced_qty", 0))
}) })

View File

@ -18,7 +18,7 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("BOM Operations Time", {}), ("BOM Operations Time", {}),
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}), ("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}), ("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
("Cost of Poor Quality Report", {}), ("Cost of Poor Quality Report", {"item": "_Test Item", "serial_no": "00"}),
("Downtime Analysis", {}), ("Downtime Analysis", {}),
( (
"Exponential Smoothing Forecasting", "Exponential Smoothing Forecasting",

View File

@ -1,5 +1,6 @@
[pre_model_sync] [pre_model_sync]
erpnext.patches.v12_0.update_is_cancelled_field erpnext.patches.v12_0.update_is_cancelled_field
erpnext.patches.v13_0.add_bin_unique_constraint
erpnext.patches.v11_0.rename_production_order_to_work_order erpnext.patches.v11_0.rename_production_order_to_work_order
erpnext.patches.v11_0.refactor_naming_series erpnext.patches.v11_0.refactor_naming_series
erpnext.patches.v11_0.refactor_autoname_naming erpnext.patches.v11_0.refactor_autoname_naming
@ -203,6 +204,7 @@ execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation")
erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020 erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020
erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020 erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020
erpnext.patches.v12_0.create_itc_reversal_custom_fields erpnext.patches.v12_0.create_itc_reversal_custom_fields
execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings")
erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020 erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020 erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
erpnext.patches.v12_0.add_taxjar_integration_field erpnext.patches.v12_0.add_taxjar_integration_field
@ -223,6 +225,7 @@ erpnext.patches.v13_0.set_youtube_video_id
erpnext.patches.v13_0.set_app_name erpnext.patches.v13_0.set_app_name
erpnext.patches.v13_0.print_uom_after_quantity_patch erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee erpnext.patches.v13_0.update_reason_for_resignation_in_employee
execute:frappe.delete_doc("Report", "Quoted Item Comparison") execute:frappe.delete_doc("Report", "Quoted Item Comparison")
@ -242,6 +245,8 @@ erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
erpnext.patches.v13_0.update_vehicle_no_reqd_condition erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17
erpnext.patches.v12_0.add_einvoice_summary_report_permissions
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
@ -255,6 +260,7 @@ erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 #17-01-2022 erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 #17-01-2022
erpnext.patches.v13_0.update_shipment_status erpnext.patches.v13_0.update_shipment_status
erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting erpnext.patches.v13_0.remove_attribute_field_from_item_variant_setting
erpnext.patches.v12_0.add_ewaybill_validity_field
erpnext.patches.v13_0.germany_make_custom_fields erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed erpnext.patches.v13_0.set_pos_closing_as_failed
@ -267,10 +273,10 @@ erpnext.patches.v13_0.bill_for_rejected_quantity_in_purchase_invoice
erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold erpnext.patches.v13_0.rename_issue_status_hold_to_on_hold
erpnext.patches.v13_0.update_response_by_variance erpnext.patches.v13_0.update_response_by_variance
erpnext.patches.v13_0.update_job_card_details erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items erpnext.patches.v13_0.update_amt_in_work_order_required_items
erpnext.patches.v12_0.show_einvoice_irn_cancelled_field
erpnext.patches.v13_0.delete_orphaned_tables 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
@ -281,7 +287,6 @@ erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022 erpnext.patches.v13_0.trim_whitespace_from_serial_nos # 16-01-2022
erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.einvoicing_deprecation_warning
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category") execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021 erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
@ -323,15 +328,18 @@ erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.update_exchange_rate_settings erpnext.patches.v13_0.update_exchange_rate_settings
erpnext.patches.v13_0.update_asset_quantity_field erpnext.patches.v13_0.update_asset_quantity_field
erpnext.patches.v13_0.delete_bank_reconciliation_detail erpnext.patches.v13_0.delete_bank_reconciliation_detail
erpnext.patches.v13_0.enable_provisional_accounting
[post_model_sync] [post_model_sync]
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
erpnext.patches.v14_0.delete_einvoicing_doctypes
erpnext.patches.v14_0.delete_shopify_doctypes erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022 erpnext.patches.v14_0.delete_hospitality_doctypes # 20-01-2022
erpnext.patches.v14_0.delete_agriculture_doctypes erpnext.patches.v14_0.delete_agriculture_doctypes
erpnext.patches.v14_0.rearrange_company_fields erpnext.patches.v14_0.rearrange_company_fields
erpnext.patches.v14_0.update_leave_notification_template erpnext.patches.v14_0.update_leave_notification_template
erpnext.patches.v14_0.restore_einvoice_fields
erpnext.patches.v13_0.update_sane_transfer_against erpnext.patches.v13_0.update_sane_transfer_against
erpnext.patches.v12_0.add_company_link_to_einvoice_settings
erpnext.patches.v14_0.migrate_cost_center_allocations

View File

@ -0,0 +1,18 @@
from __future__ import unicode_literals
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company or not frappe.db.count('E Invoice User'):
return
frappe.reload_doc("regional", "doctype", "e_invoice_user")
for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']):
company_name = frappe.db.sql("""
select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
""", (creds.get('gstin')))
if company_name and len(company_name) > 0:
frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])

View File

@ -0,0 +1,72 @@
from __future__ import unicode_literals
import json
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
# move hidden einvoice fields to a different section
custom_fields = {
'Sales Invoice': [
dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
print_hide=1, hidden=1),
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
no_copy=1, print_hide=1),
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
no_copy=1, print_hide=1),
dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code',
no_copy=1, print_hide=1, read_only=1),
dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image',
options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON',
hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1)
]
}
create_custom_fields(custom_fields, update=True)
if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'):
frappe.db.sql('''
UPDATE `tabSales Invoice` SET einvoice_status = 'Pending'
WHERE
posting_date >= '2021-04-01'
AND ifnull(irn, '') = ''
AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '')
AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export')
''')
# set appropriate statuses
frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated'
WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''')
frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled'
WHERE ifnull(irn_cancelled, 0) = 1''')
# set correct acknowledgement in e-invoices
einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice'])
if einvoices:
for inv in einvoices:
signed_einvoice = inv.get('signed_einvoice')
if signed_einvoice:
signed_einvoice = json.loads(signed_einvoice)
frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False)
frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False)

View File

@ -0,0 +1,20 @@
from __future__ import unicode_literals
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
if frappe.db.exists('Report', 'E-Invoice Summary') and \
not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')):
frappe.get_doc(dict(
doctype='Custom Role',
report='E-Invoice Summary',
roles= [
dict(role='Accounts User'),
dict(role='Accounts Manager')
]
)).insert()

View File

@ -0,0 +1,18 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
custom_fields = {
'Sales Invoice': [
dict(fieldname='eway_bill_validity', label='E-Way Bill Validity', fieldtype='Data', no_copy=1, print_hide=1,
depends_on='ewaybill', read_only=1, allow_on_submit=1, insert_after='ewaybill')
]
}
create_custom_fields(custom_fields, update=True)

View File

@ -0,0 +1,59 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.india.setup import add_permissions, add_print_formats
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
frappe.reload_doc("custom", "doctype", "custom_field")
frappe.reload_doc("regional", "doctype", "e_invoice_settings")
custom_fields = {
'Sales Invoice': [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
]
}
create_custom_fields(custom_fields, update=True)
add_permissions()
add_print_formats()
einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
t = {
'mode_of_transport': [{'default': None}],
'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}],
'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
'ewaybill': [
{'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'},
{'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'}
]
}
for field, conditions in t.items():
for c in conditions:
[(prop, value)] = c.items()
frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value)

View File

@ -0,0 +1,14 @@
from __future__ import unicode_literals
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'})
if irn_cancelled_field:
frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn')
frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0)

View File

@ -0,0 +1,63 @@
import frappe
from erpnext.stock.stock_balance import (
get_balance_qty_from_sle,
get_indented_qty,
get_ordered_qty,
get_planned_qty,
get_reserved_qty,
)
from erpnext.stock.utils import get_bin
def execute():
delete_broken_bins()
delete_and_patch_duplicate_bins()
def delete_broken_bins():
# delete useless bins
frappe.db.sql("delete from `tabBin` where item_code is null or warehouse is null")
def delete_and_patch_duplicate_bins():
duplicate_bins = frappe.db.sql("""
SELECT
item_code, warehouse, count(*) as bin_count
FROM
tabBin
GROUP BY
item_code, warehouse
HAVING
bin_count > 1
""", as_dict=1)
for duplicate_bin in duplicate_bins:
item_code = duplicate_bin.item_code
warehouse = duplicate_bin.warehouse
existing_bins = frappe.get_list("Bin",
filters={
"item_code": item_code,
"warehouse": warehouse
},
fields=["name"],
order_by="creation",)
# keep last one
existing_bins.pop()
for broken_bin in existing_bins:
frappe.delete_doc("Bin", broken_bin.name)
qty_dict = {
"reserved_qty": get_reserved_qty(item_code, warehouse),
"indented_qty": get_indented_qty(item_code, warehouse),
"ordered_qty": get_ordered_qty(item_code, warehouse),
"planned_qty": get_planned_qty(item_code, warehouse),
"actual_qty": get_balance_qty_from_sle(item_code, warehouse)
}
bin = get_bin(item_code, warehouse)
bin.update(qty_dict)
bin.update_reserved_qty_for_production()
bin.update_reserved_qty_for_sub_contracting()
bin.db_update()

View File

@ -1,9 +0,0 @@
import click
def execute():
click.secho(
"Indian E-Invoicing 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/erpnext_gst_compliance",
fg="yellow",
)

View File

@ -0,0 +1,19 @@
import frappe
def execute():
frappe.reload_doc("setup", "doctype", "company")
company = frappe.qb.DocType("Company")
frappe.qb.update(
company
).set(
company.enable_provisional_accounting_for_non_stock_items, company.enable_perpetual_inventory_for_non_stock_items
).set(
company.default_provisional_account, company.service_received_but_not_billed
).where(
company.enable_perpetual_inventory_for_non_stock_items == 1
).where(
company.service_received_but_not_billed.isnotnull()
).run()

View File

@ -3,6 +3,7 @@ from frappe import _
def execute(): def execute():
frappe.reload_doctype('Selling Settings')
selling_settings = frappe.get_single("Selling Settings") selling_settings = frappe.get_single("Selling Settings")
if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"): if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):

View File

@ -1,31 +0,0 @@
# Copyright (c) 2020, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
for document in ["bom", "bom_item", "bom_explosion_item"]:
frappe.reload_doc('manufacturing', 'doctype', document)
frappe.db.sql(" update `tabBOM` set bom_level = 0 where docstatus = 1")
bom_list = frappe.db.sql_list("""select name from `tabBOM` bom
where docstatus=1 and is_active=1 and not exists(select bom_no from `tabBOM Item`
where parent=bom.name and ifnull(bom_no, '')!='')""")
count = 0
while(count < len(bom_list)):
for parent_bom in get_parent_boms(bom_list[count]):
bom_doc = frappe.get_cached_doc("BOM", parent_bom)
bom_doc.set_bom_level(update=True)
bom_list.append(parent_bom)
count += 1
def get_parent_boms(bom_no):
return frappe.db.sql_list("""
select distinct bom_item.parent from `tabBOM Item` bom_item
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
""", bom_no)

View File

@ -3,6 +3,9 @@ import frappe
def execute(): def execute():
frappe.reload_doctype('Maintenance Visit')
frappe.reload_doctype('Maintenance Visit Purpose')
# Updates the Maintenance Schedule link to fetch serial nos # Updates the Maintenance Schedule link to fetch serial nos
from frappe.query_builder.functions import Coalesce from frappe.query_builder.functions import Coalesce
mvp = frappe.qb.DocType('Maintenance Visit Purpose') mvp = frappe.qb.DocType('Maintenance Visit Purpose')

View File

@ -1,10 +0,0 @@
import frappe
def execute():
frappe.delete_doc('DocType', 'E Invoice Settings', ignore_missing=True)
frappe.delete_doc('DocType', 'E Invoice User', ignore_missing=True)
frappe.delete_doc('Report', 'E-Invoice Summary', ignore_missing=True)
frappe.delete_doc('Print Format', 'GST E-Invoice', ignore_missing=True)
frappe.delete_doc('Custom Field', 'Sales Invoice-eway_bill_cancelled', ignore_missing=True)
frappe.delete_doc('Custom Field', 'Sales Invoice-irn_cancelled', ignore_missing=True)

View File

@ -0,0 +1,48 @@
import frappe
from frappe.utils import today
def execute():
for dt in ("cost_center_allocation", "cost_center_allocation_percentage"):
frappe.reload_doc('accounts', 'doctype', dt)
cc_allocations = get_existing_cost_center_allocations()
if cc_allocations:
create_new_cost_center_allocation_records(cc_allocations)
frappe.delete_doc('DocType', 'Distributed Cost Center', ignore_missing=True)
def create_new_cost_center_allocation_records(cc_allocations):
for main_cc, allocations in cc_allocations.items():
cca = frappe.new_doc("Cost Center Allocation")
cca.main_cost_center = main_cc
cca.valid_from = today()
for child_cc, percentage in allocations.items():
cca.append("allocation_percentages", ({
"cost_center": child_cc,
"percentage": percentage
}))
cca.save()
cca.submit()
def get_existing_cost_center_allocations():
if not frappe.db.exists("DocType", "Distributed Cost Center"):
return
par = frappe.qb.DocType("Cost Center")
child = frappe.qb.DocType("Distributed Cost Center")
records = (
frappe.qb.from_(par)
.inner_join(child).on(par.name == child.parent)
.select(par.name, child.cost_center, child.percentage_allocation)
.where(par.enable_distributed_cost_center == 1)
).run(as_dict=True)
cc_allocations = frappe._dict()
for d in records:
cc_allocations.setdefault(d.name, frappe._dict())\
.setdefault(d.cost_center, d.percentage_allocation)
return cc_allocations

View File

@ -9,8 +9,9 @@ def execute():
], as_dict=True) ], as_dict=True)
frappe.reload_doc('crm', 'doctype', 'crm_settings') frappe.reload_doc('crm', 'doctype', 'crm_settings')
frappe.db.set_value('CRM Settings', 'CRM Settings', { if settings:
'campaign_naming_by': settings.campaign_naming_by, frappe.db.set_value('CRM Settings', 'CRM Settings', {
'close_opportunity_after_days': settings.close_opportunity_after_days, 'campaign_naming_by': settings.campaign_naming_by,
'default_valid_till': settings.default_valid_till 'close_opportunity_after_days': settings.close_opportunity_after_days,
}) 'default_valid_till': settings.default_valid_till
})

View File

@ -0,0 +1,24 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from erpnext.regional.india.setup import add_permissions, add_print_formats
def execute():
# restores back the 2 custom fields that was deleted while removing e-invoicing from v14
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
custom_fields = {
'Sales Invoice': [
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
]
}
create_custom_fields(custom_fields, update=True)
add_permissions()
add_print_formats()

Some files were not shown because too many files have changed in this diff Show More