Merge branch 'develop' into independent-manu-entry
This commit is contained in:
commit
02b9541b43
27
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
27
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@ -24,20 +24,6 @@ body:
|
|||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: Version
|
|
||||||
description: Affected versions.
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- v12
|
|
||||||
- v13
|
|
||||||
- v14
|
|
||||||
- develop
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: module
|
id: module
|
||||||
attributes:
|
attributes:
|
||||||
@ -86,7 +72,7 @@ body:
|
|||||||
- manual install
|
- manual install
|
||||||
- FrappeCloud
|
- FrappeCloud
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: false
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: logs
|
id: logs
|
||||||
@ -95,12 +81,7 @@ body:
|
|||||||
description: Please copy and paste any relevant log output. This will be automatically formatted.
|
description: Please copy and paste any relevant log output. This will be automatically formatted.
|
||||||
render: shell
|
render: shell
|
||||||
|
|
||||||
|
- type: markdown
|
||||||
- type: checkboxes
|
|
||||||
id: terms
|
|
||||||
attributes:
|
attributes:
|
||||||
label: Code of Conduct
|
value: |
|
||||||
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)
|
By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)
|
||||||
options:
|
|
||||||
- label: I agree to follow this project's Code of Conduct
|
|
||||||
required: true
|
|
||||||
|
@ -1049,6 +1049,8 @@ class SalesInvoice(SellingController):
|
|||||||
frappe.flags.is_reverse_depr_entry = False
|
frappe.flags.is_reverse_depr_entry = False
|
||||||
asset.flags.ignore_validate_update_after_submit = True
|
asset.flags.ignore_validate_update_after_submit = True
|
||||||
schedule.journal_entry = None
|
schedule.journal_entry = None
|
||||||
|
depreciation_amount = self.get_depreciation_amount_in_je(reverse_journal_entry)
|
||||||
|
asset.finance_books[0].value_after_depreciation += depreciation_amount
|
||||||
asset.save()
|
asset.save()
|
||||||
|
|
||||||
def get_posting_date_of_sales_invoice(self):
|
def get_posting_date_of_sales_invoice(self):
|
||||||
@ -1071,6 +1073,12 @@ class SalesInvoice(SellingController):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def get_depreciation_amount_in_je(self, journal_entry):
|
||||||
|
if journal_entry.accounts[0].debit_in_account_currency:
|
||||||
|
return journal_entry.accounts[0].debit_in_account_currency
|
||||||
|
else:
|
||||||
|
return journal_entry.accounts[0].credit_in_account_currency
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def enable_discount_accounting(self):
|
def enable_discount_accounting(self):
|
||||||
if not hasattr(self, "_enable_discount_accounting"):
|
if not hasattr(self, "_enable_discount_accounting"):
|
||||||
|
@ -184,6 +184,8 @@ class AccountsController(TransactionBase):
|
|||||||
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
|
frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx))
|
||||||
elif getdate(self.posting_date) > getdate(d.service_end_date):
|
elif getdate(self.posting_date) > getdate(d.service_end_date):
|
||||||
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
|
frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx))
|
||||||
|
elif getdate(self.posting_date) > getdate(d.service_start_date):
|
||||||
|
frappe.throw(_("Row #{0}: Service Start Date cannot be before Invoice Posting Date").format(d.idx))
|
||||||
|
|
||||||
def validate_invoice_documents_schedule(self):
|
def validate_invoice_documents_schedule(self):
|
||||||
self.validate_payment_schedule_dates()
|
self.validate_payment_schedule_dates()
|
||||||
|
@ -23,6 +23,17 @@ class TestLead(unittest.TestCase):
|
|||||||
customer.customer_group = "_Test Customer Group"
|
customer.customer_group = "_Test Customer Group"
|
||||||
customer.insert()
|
customer.insert()
|
||||||
|
|
||||||
|
#check whether lead contact is carried forward to the customer.
|
||||||
|
contact = frappe.db.get_value('Dynamic Link', {
|
||||||
|
"parenttype": "Contact",
|
||||||
|
"link_doctype": "Lead",
|
||||||
|
"link_name": customer.lead_name,
|
||||||
|
}, "parent")
|
||||||
|
|
||||||
|
if contact:
|
||||||
|
contact_doc = frappe.get_doc("Contact", contact)
|
||||||
|
self.assertEqual(contact_doc.has_link(customer.doctype, customer.name), True)
|
||||||
|
|
||||||
def test_make_customer_from_organization(self):
|
def test_make_customer_from_organization(self):
|
||||||
from erpnext.crm.doctype.lead.lead import make_customer
|
from erpnext.crm.doctype.lead.lead import make_customer
|
||||||
|
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
{
|
|
||||||
"charts": [],
|
|
||||||
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Integrations Settings\", \"col\": 4}}]",
|
|
||||||
"creation": "2020-07-31 10:38:54.021237",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Workspace",
|
|
||||||
"for_user": "",
|
|
||||||
"hide_custom": 0,
|
|
||||||
"icon": "setting",
|
|
||||||
"idx": 0,
|
|
||||||
"label": "ERPNext Integrations Settings",
|
|
||||||
"links": [
|
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Integrations Settings",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dependencies": "",
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Woocommerce Settings",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Woocommerce Settings",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dependencies": "",
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Amazon MWS Settings",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Amazon MWS Settings",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dependencies": "",
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Plaid Settings",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Plaid Settings",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"dependencies": "",
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Exotel Settings",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "Exotel Settings",
|
|
||||||
"link_type": "DocType",
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Link"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modified": "2021-11-23 04:30:33.106991",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "ERPNext Integrations",
|
|
||||||
"name": "ERPNext Integrations Settings",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"parent_page": "",
|
|
||||||
"public": 1,
|
|
||||||
"restrict_to_domain": "",
|
|
||||||
"roles": [],
|
|
||||||
"sequence_id": 11,
|
|
||||||
"shortcuts": [],
|
|
||||||
"title": "ERPNext Integrations Settings"
|
|
||||||
}
|
|
@ -374,7 +374,7 @@ scheduler_events = {
|
|||||||
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
|
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
|
||||||
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
|
||||||
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
"erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
|
||||||
"erpnext.non_profit.doctype.membership.membership.set_expired_status"
|
"erpnext.non_profit.doctype.membership.membership.set_expired_status",
|
||||||
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
|
"erpnext.hr.doctype.interview.interview.send_daily_feedback_reminder"
|
||||||
],
|
],
|
||||||
"daily_long": [
|
"daily_long": [
|
||||||
|
@ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", {
|
|||||||
['docstatus', '=', 1],
|
['docstatus', '=', 1],
|
||||||
['employee', '=', frm.doc.employee],
|
['employee', '=', frm.doc.employee],
|
||||||
['paid_amount', '>', 0],
|
['paid_amount', '>', 0],
|
||||||
['paid_amount', '>', 'claimed_amount']
|
['status', '!=', 'Claimed']
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -10,15 +10,17 @@ from erpnext.accounts.doctype.account.test_account import create_account
|
|||||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
|
from erpnext.hr.doctype.expense_claim.expense_claim import make_bank_entry
|
||||||
|
|
||||||
test_records = frappe.get_test_records('Expense Claim')
|
|
||||||
test_dependencies = ['Employee']
|
test_dependencies = ['Employee']
|
||||||
company_name = '_Test Company 4'
|
company_name = '_Test Company 3'
|
||||||
|
|
||||||
|
|
||||||
class TestExpenseClaim(unittest.TestCase):
|
class TestExpenseClaim(unittest.TestCase):
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def test_total_expense_claim_for_project(self):
|
def test_total_expense_claim_for_project(self):
|
||||||
frappe.db.sql("""delete from `tabTask` where project = "_Test Project 1" """)
|
frappe.db.sql("""delete from `tabTask`""")
|
||||||
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
|
frappe.db.sql("""delete from `tabProject`""")
|
||||||
frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
|
frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
|
||||||
|
|
||||||
project = frappe.get_doc({
|
project = frappe.get_doc({
|
||||||
@ -37,12 +39,12 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
task_name = task.name
|
task_name = task.name
|
||||||
payable_account = get_payable_account(company_name)
|
payable_account = get_payable_account(company_name)
|
||||||
|
|
||||||
make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name)
|
make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3", project.name, task_name)
|
||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
|
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
|
||||||
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
|
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
|
||||||
|
|
||||||
expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name)
|
expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC3", project.name, task_name)
|
||||||
|
|
||||||
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
|
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
|
||||||
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700)
|
self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700)
|
||||||
@ -54,7 +56,7 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
|
|
||||||
def test_expense_claim_status(self):
|
def test_expense_claim_status(self):
|
||||||
payable_account = get_payable_account(company_name)
|
payable_account = get_payable_account(company_name)
|
||||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4")
|
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3")
|
||||||
|
|
||||||
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
|
je_dict = make_bank_entry("Expense Claim", expense_claim.name)
|
||||||
je = frappe.get_doc(je_dict)
|
je = frappe.get_doc(je_dict)
|
||||||
@ -73,7 +75,7 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
def test_expense_claim_gl_entry(self):
|
def test_expense_claim_gl_entry(self):
|
||||||
payable_account = get_payable_account(company_name)
|
payable_account = get_payable_account(company_name)
|
||||||
taxes = generate_taxes()
|
taxes = generate_taxes()
|
||||||
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4",
|
expense_claim = make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC3",
|
||||||
do_not_submit=True, taxes=taxes)
|
do_not_submit=True, taxes=taxes)
|
||||||
expense_claim.submit()
|
expense_claim.submit()
|
||||||
|
|
||||||
@ -84,9 +86,9 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
self.assertTrue(gl_entries)
|
self.assertTrue(gl_entries)
|
||||||
|
|
||||||
expected_values = dict((d[0], d) for d in [
|
expected_values = dict((d[0], d) for d in [
|
||||||
['Output Tax CGST - _TC4',18.0, 0.0],
|
['Output Tax CGST - _TC3',18.0, 0.0],
|
||||||
[payable_account, 0.0, 218.0],
|
[payable_account, 0.0, 218.0],
|
||||||
["Travel Expenses - _TC4", 200.0, 0.0]
|
["Travel Expenses - _TC3", 200.0, 0.0]
|
||||||
])
|
])
|
||||||
|
|
||||||
for gle in gl_entries:
|
for gle in gl_entries:
|
||||||
@ -102,7 +104,7 @@ class TestExpenseClaim(unittest.TestCase):
|
|||||||
"payable_account": payable_account,
|
"payable_account": payable_account,
|
||||||
"approval_status": "Rejected",
|
"approval_status": "Rejected",
|
||||||
"expenses":
|
"expenses":
|
||||||
[{ "expense_type": "Travel", "default_account": "Travel Expenses - _TC4", "amount": 300, "sanctioned_amount": 200 }]
|
[{"expense_type": "Travel", "default_account": "Travel Expenses - _TC3", "amount": 300, "sanctioned_amount": 200}]
|
||||||
})
|
})
|
||||||
expense_claim.submit()
|
expense_claim.submit()
|
||||||
|
|
||||||
|
@ -1 +0,0 @@
|
|||||||
[]
|
|
@ -4,6 +4,7 @@ import frappe
|
|||||||
from frappe.utils import add_days, add_months, getdate, nowdate
|
from frappe.utils import add_days, add_months, getdate, nowdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
|
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
|
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation
|
||||||
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
from erpnext.hr.doctype.leave_type.test_leave_type import create_leave_type
|
||||||
|
|
||||||
@ -13,16 +14,19 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
frappe.db.sql("delete from `tabLeave Period`")
|
frappe.db.sql("delete from `tabLeave Period`")
|
||||||
|
|
||||||
def test_overlapping_allocation(self):
|
emp_id = make_employee("test_emp_leave_allocation@salary.com")
|
||||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
cls.employee = frappe.get_doc("Employee", emp_id)
|
||||||
|
|
||||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
|
def test_overlapping_allocation(self):
|
||||||
leaves = [
|
leaves = [
|
||||||
{
|
{
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
"__islocal": 1,
|
"__islocal": 1,
|
||||||
"employee": employee.name,
|
"employee": self.employee.name,
|
||||||
"employee_name": employee.employee_name,
|
"employee_name": self.employee.employee_name,
|
||||||
"leave_type": "_Test Leave Type",
|
"leave_type": "_Test Leave Type",
|
||||||
"from_date": getdate("2015-10-01"),
|
"from_date": getdate("2015-10-01"),
|
||||||
"to_date": getdate("2015-10-31"),
|
"to_date": getdate("2015-10-31"),
|
||||||
@ -32,8 +36,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
{
|
{
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
"__islocal": 1,
|
"__islocal": 1,
|
||||||
"employee": employee.name,
|
"employee": self.employee.name,
|
||||||
"employee_name": employee.employee_name,
|
"employee_name": self.employee.employee_name,
|
||||||
"leave_type": "_Test Leave Type",
|
"leave_type": "_Test Leave Type",
|
||||||
"from_date": getdate("2015-09-01"),
|
"from_date": getdate("2015-09-01"),
|
||||||
"to_date": getdate("2015-11-30"),
|
"to_date": getdate("2015-11-30"),
|
||||||
@ -45,13 +49,11 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save)
|
self.assertRaises(frappe.ValidationError, frappe.get_doc(leaves[1]).save)
|
||||||
|
|
||||||
def test_invalid_period(self):
|
def test_invalid_period(self):
|
||||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
|
||||||
|
|
||||||
doc = frappe.get_doc({
|
doc = frappe.get_doc({
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
"__islocal": 1,
|
"__islocal": 1,
|
||||||
"employee": employee.name,
|
"employee": self.employee.name,
|
||||||
"employee_name": employee.employee_name,
|
"employee_name": self.employee.employee_name,
|
||||||
"leave_type": "_Test Leave Type",
|
"leave_type": "_Test Leave Type",
|
||||||
"from_date": getdate("2015-09-30"),
|
"from_date": getdate("2015-09-30"),
|
||||||
"to_date": getdate("2015-09-1"),
|
"to_date": getdate("2015-09-1"),
|
||||||
@ -62,23 +64,21 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
self.assertRaises(frappe.ValidationError, doc.save)
|
self.assertRaises(frappe.ValidationError, doc.save)
|
||||||
|
|
||||||
def test_allocated_leave_days_over_period(self):
|
def test_allocated_leave_days_over_period(self):
|
||||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
|
||||||
doc = frappe.get_doc({
|
doc = frappe.get_doc({
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
"__islocal": 1,
|
"__islocal": 1,
|
||||||
"employee": employee.name,
|
"employee": self.employee.name,
|
||||||
"employee_name": employee.employee_name,
|
"employee_name": self.employee.employee_name,
|
||||||
"leave_type": "_Test Leave Type",
|
"leave_type": "_Test Leave Type",
|
||||||
"from_date": getdate("2015-09-1"),
|
"from_date": getdate("2015-09-1"),
|
||||||
"to_date": getdate("2015-09-30"),
|
"to_date": getdate("2015-09-30"),
|
||||||
"new_leaves_allocated": 35
|
"new_leaves_allocated": 35
|
||||||
})
|
})
|
||||||
|
|
||||||
# allocated leave more than period
|
# allocated leave more than period
|
||||||
self.assertRaises(frappe.ValidationError, doc.save)
|
self.assertRaises(frappe.ValidationError, doc.save)
|
||||||
|
|
||||||
def test_carry_forward_calculation(self):
|
def test_carry_forward_calculation(self):
|
||||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
|
||||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
|
||||||
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
leave_type = create_leave_type(leave_type_name="_Test_CF_leave", is_carry_forward=1)
|
||||||
leave_type.maximum_carry_forwarded_leaves = 10
|
leave_type.maximum_carry_forwarded_leaves = 10
|
||||||
leave_type.max_leaves_allowed = 30
|
leave_type.max_leaves_allowed = 30
|
||||||
@ -86,6 +86,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
|
|
||||||
# initial leave allocation = 15
|
# initial leave allocation = 15
|
||||||
leave_allocation = create_leave_allocation(
|
leave_allocation = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
leave_type="_Test_CF_leave",
|
leave_type="_Test_CF_leave",
|
||||||
from_date=add_months(nowdate(), -12),
|
from_date=add_months(nowdate(), -12),
|
||||||
to_date=add_months(nowdate(), -1),
|
to_date=add_months(nowdate(), -1),
|
||||||
@ -95,6 +97,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
# carry forwarded leaves considering maximum_carry_forwarded_leaves
|
# carry forwarded leaves considering maximum_carry_forwarded_leaves
|
||||||
# new_leaves = 15, carry_forwarded = 10
|
# new_leaves = 15, carry_forwarded = 10
|
||||||
leave_allocation_1 = create_leave_allocation(
|
leave_allocation_1 = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
leave_type="_Test_CF_leave",
|
leave_type="_Test_CF_leave",
|
||||||
carry_forward=1)
|
carry_forward=1)
|
||||||
leave_allocation_1.submit()
|
leave_allocation_1.submit()
|
||||||
@ -106,6 +110,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
# carry forwarded leaves considering max_leave_allowed
|
# carry forwarded leaves considering max_leave_allowed
|
||||||
# max_leave_allowed = 30, new_leaves = 25, carry_forwarded = 5
|
# max_leave_allowed = 30, new_leaves = 25, carry_forwarded = 5
|
||||||
leave_allocation_2 = create_leave_allocation(
|
leave_allocation_2 = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
leave_type="_Test_CF_leave",
|
leave_type="_Test_CF_leave",
|
||||||
carry_forward=1,
|
carry_forward=1,
|
||||||
new_leaves_allocated=25)
|
new_leaves_allocated=25)
|
||||||
@ -114,8 +120,6 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
self.assertEqual(leave_allocation_2.unused_leaves, 5)
|
self.assertEqual(leave_allocation_2.unused_leaves, 5)
|
||||||
|
|
||||||
def test_carry_forward_leaves_expiry(self):
|
def test_carry_forward_leaves_expiry(self):
|
||||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
|
||||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
|
||||||
leave_type = create_leave_type(
|
leave_type = create_leave_type(
|
||||||
leave_type_name="_Test_CF_leave_expiry",
|
leave_type_name="_Test_CF_leave_expiry",
|
||||||
is_carry_forward=1,
|
is_carry_forward=1,
|
||||||
@ -124,6 +128,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
|
|
||||||
# initial leave allocation
|
# initial leave allocation
|
||||||
leave_allocation = create_leave_allocation(
|
leave_allocation = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
leave_type="_Test_CF_leave_expiry",
|
leave_type="_Test_CF_leave_expiry",
|
||||||
from_date=add_months(nowdate(), -24),
|
from_date=add_months(nowdate(), -24),
|
||||||
to_date=add_months(nowdate(), -12),
|
to_date=add_months(nowdate(), -12),
|
||||||
@ -131,6 +137,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
|
|
||||||
leave_allocation = create_leave_allocation(
|
leave_allocation = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
leave_type="_Test_CF_leave_expiry",
|
leave_type="_Test_CF_leave_expiry",
|
||||||
from_date=add_days(nowdate(), -90),
|
from_date=add_days(nowdate(), -90),
|
||||||
to_date=add_days(nowdate(), 100),
|
to_date=add_days(nowdate(), 100),
|
||||||
@ -142,6 +150,8 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
|
|
||||||
# leave allocation with carry forward of only new leaves allocated
|
# leave allocation with carry forward of only new leaves allocated
|
||||||
leave_allocation_1 = create_leave_allocation(
|
leave_allocation_1 = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name,
|
||||||
leave_type="_Test_CF_leave_expiry",
|
leave_type="_Test_CF_leave_expiry",
|
||||||
carry_forward=1,
|
carry_forward=1,
|
||||||
from_date=add_months(nowdate(), 6),
|
from_date=add_months(nowdate(), 6),
|
||||||
@ -151,9 +161,10 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated)
|
self.assertEqual(leave_allocation_1.unused_leaves, leave_allocation.new_leaves_allocated)
|
||||||
|
|
||||||
def test_creation_of_leave_ledger_entry_on_submit(self):
|
def test_creation_of_leave_ledger_entry_on_submit(self):
|
||||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
leave_allocation = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
leave_allocation = create_leave_allocation()
|
employee_name=self.employee.employee_name
|
||||||
|
)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
|
|
||||||
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name))
|
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_allocation.name))
|
||||||
@ -168,10 +179,10 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
|
self.assertFalse(frappe.db.exists("Leave Ledger Entry", {'transaction_name':leave_allocation.name}))
|
||||||
|
|
||||||
def test_leave_addition_after_submit(self):
|
def test_leave_addition_after_submit(self):
|
||||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
leave_allocation = create_leave_allocation(
|
||||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name
|
||||||
leave_allocation = create_leave_allocation()
|
)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||||
leave_allocation.new_leaves_allocated = 40
|
leave_allocation.new_leaves_allocated = 40
|
||||||
@ -179,44 +190,55 @@ class TestLeaveAllocation(unittest.TestCase):
|
|||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 40)
|
||||||
|
|
||||||
def test_leave_subtraction_after_submit(self):
|
def test_leave_subtraction_after_submit(self):
|
||||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
leave_allocation = create_leave_allocation(
|
||||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
employee=self.employee.name,
|
||||||
leave_allocation = create_leave_allocation()
|
employee_name=self.employee.employee_name
|
||||||
|
)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||||
leave_allocation.new_leaves_allocated = 10
|
leave_allocation.new_leaves_allocated = 10
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 10)
|
||||||
|
|
||||||
def test_against_leave_application_validation_after_submit(self):
|
def test_validation_against_leave_application_after_submit(self):
|
||||||
frappe.db.sql("delete from `tabLeave Allocation`")
|
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||||
frappe.db.sql("delete from `tabLeave Ledger Entry`")
|
|
||||||
|
|
||||||
leave_allocation = create_leave_allocation()
|
make_holiday_list()
|
||||||
|
frappe.db.set_value("Company", self.employee.company, "default_holiday_list", "Salary Slip Test Holiday List")
|
||||||
|
|
||||||
|
leave_allocation = create_leave_allocation(
|
||||||
|
employee=self.employee.name,
|
||||||
|
employee_name=self.employee.employee_name
|
||||||
|
)
|
||||||
leave_allocation.submit()
|
leave_allocation.submit()
|
||||||
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
self.assertTrue(leave_allocation.total_leaves_allocated, 15)
|
||||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
|
||||||
leave_application = frappe.get_doc({
|
leave_application = frappe.get_doc({
|
||||||
"doctype": 'Leave Application',
|
"doctype": 'Leave Application',
|
||||||
"employee": employee.name,
|
"employee": self.employee.name,
|
||||||
"leave_type": "_Test Leave Type",
|
"leave_type": "_Test Leave Type",
|
||||||
"from_date": add_months(nowdate(), 2),
|
"from_date": add_months(nowdate(), 2),
|
||||||
"to_date": add_months(add_days(nowdate(), 10), 2),
|
"to_date": add_months(add_days(nowdate(), 10), 2),
|
||||||
"company": erpnext.get_default_company() or "_Test Company",
|
"company": self.employee.company,
|
||||||
"docstatus": 1,
|
"docstatus": 1,
|
||||||
"status": "Approved",
|
"status": "Approved",
|
||||||
"leave_approver": 'test@example.com'
|
"leave_approver": 'test@example.com'
|
||||||
})
|
})
|
||||||
leave_application.submit()
|
leave_application.submit()
|
||||||
leave_allocation.new_leaves_allocated = 8
|
leave_application.reload()
|
||||||
leave_allocation.total_leaves_allocated = 8
|
|
||||||
|
# allocate less leaves than the ones which are already approved
|
||||||
|
leave_allocation.new_leaves_allocated = leave_application.total_leave_days - 1
|
||||||
|
leave_allocation.total_leaves_allocated = leave_application.total_leave_days - 1
|
||||||
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
|
self.assertRaises(frappe.ValidationError, leave_allocation.submit)
|
||||||
|
|
||||||
def create_leave_allocation(**args):
|
def create_leave_allocation(**args):
|
||||||
args = frappe._dict(args)
|
args = frappe._dict(args)
|
||||||
|
|
||||||
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
|
emp_id = make_employee("test_emp_leave_allocation@salary.com")
|
||||||
leave_allocation = frappe.get_doc({
|
employee = frappe.get_doc("Employee", emp_id)
|
||||||
|
|
||||||
|
return frappe.get_doc({
|
||||||
"doctype": "Leave Allocation",
|
"doctype": "Leave Allocation",
|
||||||
"__islocal": 1,
|
"__islocal": 1,
|
||||||
"employee": args.employee or employee.name,
|
"employee": args.employee or employee.name,
|
||||||
@ -227,6 +249,5 @@ def create_leave_allocation(**args):
|
|||||||
"carry_forward": args.carry_forward or 0,
|
"carry_forward": args.carry_forward or 0,
|
||||||
"to_date": args.to_date or add_months(nowdate(), 12)
|
"to_date": args.to_date or add_months(nowdate(), 12)
|
||||||
})
|
})
|
||||||
return leave_allocation
|
|
||||||
|
|
||||||
test_dependencies = ["Employee", "Leave Type"]
|
test_dependencies = ["Employee", "Leave Type"]
|
||||||
|
@ -318,3 +318,5 @@ erpnext.patches.v14_0.migrate_crm_settings
|
|||||||
erpnext.patches.v13_0.rename_ksa_qr_field
|
erpnext.patches.v13_0.rename_ksa_qr_field
|
||||||
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
|
||||||
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
|
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template
|
||||||
|
erpnext.patches.v13_0.update_tax_category_for_rcm
|
||||||
|
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
|
||||||
|
@ -22,4 +22,5 @@ def execute():
|
|||||||
|
|
||||||
delivery_settings = frappe.get_doc("Delivery Settings")
|
delivery_settings = frappe.get_doc("Delivery Settings")
|
||||||
delivery_settings.dispatch_template = _("Dispatch Notification")
|
delivery_settings.dispatch_template = _("Dispatch Notification")
|
||||||
|
delivery_settings.flags.ignore_links = True
|
||||||
delivery_settings.save()
|
delivery_settings.save()
|
||||||
|
@ -97,6 +97,8 @@ def execute():
|
|||||||
'itc_central_tax': 0,
|
'itc_central_tax': 0,
|
||||||
'itc_cess_amount': 0
|
'itc_cess_amount': 0
|
||||||
})
|
})
|
||||||
|
if not gst_accounts:
|
||||||
|
continue
|
||||||
|
|
||||||
if d.account_head in gst_accounts.get('igst_account'):
|
if d.account_head in gst_accounts.get('igst_account'):
|
||||||
amount_map[d.parent]['itc_integrated_tax'] += d.amount
|
amount_map[d.parent]['itc_integrated_tax'] += d.amount
|
||||||
|
@ -32,4 +32,5 @@ def execute():
|
|||||||
hr_settings = frappe.get_doc('HR Settings')
|
hr_settings = frappe.get_doc('HR Settings')
|
||||||
hr_settings.interview_reminder_template = _('Interview Reminder')
|
hr_settings.interview_reminder_template = _('Interview Reminder')
|
||||||
hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
|
hr_settings.feedback_reminder_notification_template = _('Interview Feedback Reminder')
|
||||||
|
hr_settings.flags.ignore_links = True
|
||||||
hr_settings.save()
|
hr_settings.save()
|
||||||
|
31
erpnext/patches/v13_0/update_tax_category_for_rcm.py
Normal file
31
erpnext/patches/v13_0/update_tax_category_for_rcm.py
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
|
from erpnext.regional.india import states
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
company = frappe.get_all('Company', filters = {'country': 'India'})
|
||||||
|
if not company:
|
||||||
|
return
|
||||||
|
|
||||||
|
create_custom_fields({
|
||||||
|
'Tax Category': [
|
||||||
|
dict(fieldname='is_inter_state', label='Is Inter State',
|
||||||
|
fieldtype='Check', insert_after='disabled', print_hide=1),
|
||||||
|
dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check',
|
||||||
|
insert_after='is_inter_state', print_hide=1),
|
||||||
|
dict(fieldname='tax_category_column_break', fieldtype='Column Break',
|
||||||
|
insert_after='is_reverse_charge'),
|
||||||
|
dict(fieldname='gst_state', label='Source State', fieldtype='Select',
|
||||||
|
options='\n'.join(states), insert_after='company')
|
||||||
|
]
|
||||||
|
}, update=True)
|
||||||
|
|
||||||
|
tax_category = frappe.qb.DocType("Tax Category")
|
||||||
|
|
||||||
|
frappe.qb.update(tax_category).set(
|
||||||
|
tax_category.is_reverse_charge, 1
|
||||||
|
).where(
|
||||||
|
tax_category.name.isin(['Reverse Charge Out-State', 'Reverse Charge In-State'])
|
||||||
|
).run()
|
@ -24,4 +24,5 @@ def execute():
|
|||||||
|
|
||||||
hr_settings = frappe.get_doc("HR Settings")
|
hr_settings = frappe.get_doc("HR Settings")
|
||||||
hr_settings.exit_questionnaire_notification_template = template
|
hr_settings.exit_questionnaire_notification_template = template
|
||||||
|
hr_settings.flags.ignore_links = True
|
||||||
hr_settings.save()
|
hr_settings.save()
|
||||||
|
@ -59,22 +59,16 @@ frappe.ui.form.on("Project", {
|
|||||||
|
|
||||||
frm.trigger('show_dashboard');
|
frm.trigger('show_dashboard');
|
||||||
}
|
}
|
||||||
frm.events.set_buttons(frm);
|
frm.trigger("set_custom_buttons");
|
||||||
},
|
},
|
||||||
|
|
||||||
set_buttons: function(frm) {
|
set_custom_buttons: function(frm) {
|
||||||
if (!frm.is_new()) {
|
if (!frm.is_new()) {
|
||||||
frm.add_custom_button(__('Duplicate Project with Tasks'), () => {
|
frm.add_custom_button(__('Duplicate Project with Tasks'), () => {
|
||||||
frm.events.create_duplicate(frm);
|
frm.events.create_duplicate(frm);
|
||||||
});
|
}, __("Actions"));
|
||||||
|
|
||||||
frm.add_custom_button(__('Completed'), () => {
|
frm.trigger("set_project_status_button");
|
||||||
frm.events.set_status(frm, 'Completed');
|
|
||||||
}, __('Set Status'));
|
|
||||||
|
|
||||||
frm.add_custom_button(__('Cancelled'), () => {
|
|
||||||
frm.events.set_status(frm, 'Cancelled');
|
|
||||||
}, __('Set Status'));
|
|
||||||
|
|
||||||
|
|
||||||
if (frappe.model.can_read("Task")) {
|
if (frappe.model.can_read("Task")) {
|
||||||
@ -83,7 +77,7 @@ frappe.ui.form.on("Project", {
|
|||||||
"project": frm.doc.name
|
"project": frm.doc.name
|
||||||
};
|
};
|
||||||
frappe.set_route("List", "Task", "Gantt");
|
frappe.set_route("List", "Task", "Gantt");
|
||||||
});
|
}, __("View"));
|
||||||
|
|
||||||
frm.add_custom_button(__("Kanban Board"), () => {
|
frm.add_custom_button(__("Kanban Board"), () => {
|
||||||
frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
|
frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
|
||||||
@ -91,13 +85,35 @@ frappe.ui.form.on("Project", {
|
|||||||
}).then(() => {
|
}).then(() => {
|
||||||
frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
|
frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
|
||||||
});
|
});
|
||||||
});
|
}, __("View"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
set_project_status_button: function(frm) {
|
||||||
|
frm.add_custom_button(__('Set Project Status'), () => {
|
||||||
|
let d = new frappe.ui.Dialog({
|
||||||
|
"title": __("Set Project Status"),
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Status",
|
||||||
|
"reqd": 1,
|
||||||
|
"options": "Completed\nCancelled",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
primary_action: function() {
|
||||||
|
frm.events.set_status(frm, d.get_values().status);
|
||||||
|
d.hide();
|
||||||
|
},
|
||||||
|
primary_action_label: __("Set Project Status")
|
||||||
|
}).show();
|
||||||
|
}, __("Actions"));
|
||||||
|
},
|
||||||
|
|
||||||
create_duplicate: function(frm) {
|
create_duplicate: function(frm) {
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
frappe.prompt('Project Name', (data) => {
|
frappe.prompt('Project Name', (data) => {
|
||||||
@ -117,7 +133,9 @@ frappe.ui.form.on("Project", {
|
|||||||
set_status: function(frm, status) {
|
set_status: function(frm, status) {
|
||||||
frappe.confirm(__('Set Project and all Tasks to status {0}?', [status.bold()]), () => {
|
frappe.confirm(__('Set Project and all Tasks to status {0}?', [status.bold()]), () => {
|
||||||
frappe.xcall('erpnext.projects.doctype.project.project.set_project_status',
|
frappe.xcall('erpnext.projects.doctype.project.project.set_project_status',
|
||||||
{project: frm.doc.name, status: status}).then(() => { /* page will auto reload */ });
|
{project: frm.doc.name, status: status}).then(() => {
|
||||||
|
frm.reload_doc();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -680,7 +680,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
var item = frappe.get_doc(cdt, cdn);
|
var item = frappe.get_doc(cdt, cdn);
|
||||||
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
|
frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
|
||||||
|
|
||||||
// check if child doctype is Sales Order Item/Qutation Item and calculate the rate
|
// check if child doctype is Sales Order Item/Quotation Item and calculate the rate
|
||||||
if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Purchase Receipt Item"]), cdt)
|
if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Purchase Receipt Item"]), cdt)
|
||||||
this.apply_pricing_rule_on_item(item);
|
this.apply_pricing_rule_on_item(item);
|
||||||
else
|
else
|
||||||
@ -1582,25 +1582,27 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
|
|
||||||
_set_values_for_item_list(children) {
|
_set_values_for_item_list(children) {
|
||||||
var me = this;
|
var me = this;
|
||||||
var price_list_rate_changed = false;
|
|
||||||
var items_rule_dict = {};
|
var items_rule_dict = {};
|
||||||
|
|
||||||
for(var i=0, l=children.length; i<l; i++) {
|
for(var i=0, l=children.length; i<l; i++) {
|
||||||
var d = children[i] ;
|
var d = children[i] ;
|
||||||
|
let item_row = frappe.get_doc(d.doctype, d.name);
|
||||||
var existing_pricing_rule = frappe.model.get_value(d.doctype, d.name, "pricing_rules");
|
var existing_pricing_rule = frappe.model.get_value(d.doctype, d.name, "pricing_rules");
|
||||||
for(var k in d) {
|
for(var k in d) {
|
||||||
var v = d[k];
|
var v = d[k];
|
||||||
if (["doctype", "name"].indexOf(k)===-1) {
|
if (["doctype", "name"].indexOf(k)===-1) {
|
||||||
if(k=="price_list_rate") {
|
if(k=="price_list_rate") {
|
||||||
if(flt(v) != flt(d.price_list_rate)) price_list_rate_changed = true;
|
item_row['rate'] = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (k !== 'free_item_data') {
|
if (k !== 'free_item_data') {
|
||||||
frappe.model.set_value(d.doctype, d.name, k, v);
|
item_row[k] = v;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
frappe.model.round_floats_in(item_row, ["price_list_rate", "discount_percentage"]);
|
||||||
|
|
||||||
// if pricing rule set as blank from an existing value, apply price_list
|
// if pricing rule set as blank from an existing value, apply price_list
|
||||||
if(!me.frm.doc.ignore_pricing_rule && existing_pricing_rule && !d.pricing_rules) {
|
if(!me.frm.doc.ignore_pricing_rule && existing_pricing_rule && !d.pricing_rules) {
|
||||||
me.apply_price_list(frappe.get_doc(d.doctype, d.name));
|
me.apply_price_list(frappe.get_doc(d.doctype, d.name));
|
||||||
@ -1617,9 +1619,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
me.frm.refresh_field('items');
|
||||||
me.apply_rule_on_other_items(items_rule_dict);
|
me.apply_rule_on_other_items(items_rule_dict);
|
||||||
|
|
||||||
if(!price_list_rate_changed) me.calculate_taxes_and_totals();
|
me.calculate_taxes_and_totals();
|
||||||
}
|
}
|
||||||
|
|
||||||
apply_rule_on_other_items(args) {
|
apply_rule_on_other_items(args) {
|
||||||
|
@ -277,8 +277,10 @@ def get_custom_fields():
|
|||||||
inter_state_gst_field = [
|
inter_state_gst_field = [
|
||||||
dict(fieldname='is_inter_state', label='Is Inter State',
|
dict(fieldname='is_inter_state', label='Is Inter State',
|
||||||
fieldtype='Check', insert_after='disabled', print_hide=1),
|
fieldtype='Check', insert_after='disabled', print_hide=1),
|
||||||
|
dict(fieldname='is_reverse_charge', label='Is Reverse Charge', fieldtype='Check',
|
||||||
|
insert_after='is_inter_state', print_hide=1),
|
||||||
dict(fieldname='tax_category_column_break', fieldtype='Column Break',
|
dict(fieldname='tax_category_column_break', fieldtype='Column Break',
|
||||||
insert_after='is_inter_state'),
|
insert_after='is_reverse_charge'),
|
||||||
dict(fieldname='gst_state', label='Source State', fieldtype='Select',
|
dict(fieldname='gst_state', label='Source State', fieldtype='Select',
|
||||||
options='\n'.join(states), insert_after='company')
|
options='\n'.join(states), insert_after='company')
|
||||||
]
|
]
|
||||||
|
@ -67,7 +67,8 @@ def validate_pan_for_india(doc, method):
|
|||||||
frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
|
frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
|
||||||
|
|
||||||
def validate_tax_category(doc, method):
|
def validate_tax_category(doc, method):
|
||||||
if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
|
if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state,
|
||||||
|
'is_reverse_charge': doc.is_reverse_charge}):
|
||||||
if doc.is_inter_state:
|
if doc.is_inter_state:
|
||||||
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
|
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
|
||||||
else:
|
else:
|
||||||
@ -264,7 +265,7 @@ def get_tax_template_based_on_category(master_doctype, company, party_details):
|
|||||||
|
|
||||||
def get_tax_template(master_doctype, company, is_inter_state, state_code):
|
def get_tax_template(master_doctype, company, is_inter_state, state_code):
|
||||||
tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
|
tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
|
||||||
filters = {'is_inter_state': is_inter_state})
|
filters = {'is_inter_state': is_inter_state, 'is_reverse_charge': 0})
|
||||||
|
|
||||||
default_tax = ''
|
default_tax = ''
|
||||||
|
|
||||||
|
@ -196,7 +196,6 @@ class Customer(TransactionBase):
|
|||||||
if not lead.lead_name:
|
if not lead.lead_name:
|
||||||
frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name))
|
frappe.throw(_("Please mention the Lead Name in Lead {0}").format(self.lead_name))
|
||||||
|
|
||||||
if lead.company_name:
|
|
||||||
contact_names = frappe.get_all('Dynamic Link', filters={
|
contact_names = frappe.get_all('Dynamic Link', filters={
|
||||||
"parenttype":"Contact",
|
"parenttype":"Contact",
|
||||||
"link_doctype":"Lead",
|
"link_doctype":"Lead",
|
||||||
@ -209,7 +208,7 @@ class Customer(TransactionBase):
|
|||||||
contact.append('links', dict(link_doctype='Customer', link_name=self.name))
|
contact.append('links', dict(link_doctype='Customer', link_name=self.name))
|
||||||
contact.save(ignore_permissions=self.flags.ignore_permissions)
|
contact.save(ignore_permissions=self.flags.ignore_permissions)
|
||||||
|
|
||||||
else:
|
if not contact_names:
|
||||||
lead.lead_name = lead.lead_name.lstrip().split(" ")
|
lead.lead_name = lead.lead_name.lstrip().split(" ")
|
||||||
lead.first_name = lead.lead_name[0]
|
lead.first_name = lead.lead_name[0]
|
||||||
lead.last_name = " ".join(lead.lead_name[1:])
|
lead.last_name = " ".join(lead.lead_name[1:])
|
||||||
|
@ -61,6 +61,7 @@ def get_data(conditions, filters):
|
|||||||
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
IF(so.status in ('Completed','To Bill'), 0, (SELECT delay_days)) as delay,
|
||||||
soi.qty, soi.delivered_qty,
|
soi.qty, soi.delivered_qty,
|
||||||
(soi.qty - soi.delivered_qty) AS pending_qty,
|
(soi.qty - soi.delivered_qty) AS pending_qty,
|
||||||
|
IF((SELECT pending_qty) = 0, (TO_SECONDS(Max(dn.posting_date))-TO_SECONDS(so.transaction_date)), 0) as time_taken_to_deliver,
|
||||||
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
IFNULL(SUM(sii.qty), 0) as billed_qty,
|
||||||
soi.base_amount as amount,
|
soi.base_amount as amount,
|
||||||
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
(soi.delivered_qty * soi.base_rate) as delivered_qty_amount,
|
||||||
@ -70,9 +71,13 @@ def get_data(conditions, filters):
|
|||||||
so.company, soi.name
|
so.company, soi.name
|
||||||
FROM
|
FROM
|
||||||
`tabSales Order` so,
|
`tabSales Order` so,
|
||||||
`tabSales Order Item` soi
|
(`tabSales Order Item` soi
|
||||||
LEFT JOIN `tabSales Invoice Item` sii
|
LEFT JOIN `tabSales Invoice Item` sii
|
||||||
ON sii.so_detail = soi.name and sii.docstatus = 1
|
ON sii.so_detail = soi.name and sii.docstatus = 1)
|
||||||
|
LEFT JOIN `tabDelivery Note Item` dni
|
||||||
|
on dni.so_detail = soi.name
|
||||||
|
RIGHT JOIN `tabDelivery Note` dn
|
||||||
|
on dni.parent = dn.name and dn.docstatus = 1
|
||||||
WHERE
|
WHERE
|
||||||
soi.parent = so.name
|
soi.parent = so.name
|
||||||
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
and so.status not in ('Stopped', 'Closed', 'On Hold')
|
||||||
@ -259,6 +264,12 @@ def get_columns(filters):
|
|||||||
"fieldname": "delay",
|
"fieldname": "delay",
|
||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"width": 100
|
"width": 100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": _("Time Taken to Deliver"),
|
||||||
|
"fieldname": "time_taken_to_deliver",
|
||||||
|
"fieldtype": "Duration",
|
||||||
|
"width": 100
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
if not filters.get("group_by_so"):
|
if not filters.get("group_by_so"):
|
||||||
|
@ -1178,11 +1178,13 @@
|
|||||||
{
|
{
|
||||||
"title": "Reverse Charge In-State",
|
"title": "Reverse Charge In-State",
|
||||||
"is_inter_state": 0,
|
"is_inter_state": 0,
|
||||||
|
"is_reverse_charge": 1,
|
||||||
"gst_state": ""
|
"gst_state": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"title": "Reverse Charge Out-State",
|
"title": "Reverse Charge Out-State",
|
||||||
"is_inter_state": 1,
|
"is_inter_state": 1,
|
||||||
|
"is_reverse_charge": 1,
|
||||||
"gst_state": ""
|
"gst_state": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -150,7 +150,7 @@
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_filter": 1,
|
"in_filter": 1,
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Actual Quantity",
|
"label": "Qty Change",
|
||||||
"oldfieldname": "actual_qty",
|
"oldfieldname": "actual_qty",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
@ -189,7 +189,7 @@
|
|||||||
"fieldname": "qty_after_transaction",
|
"fieldname": "qty_after_transaction",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"in_filter": 1,
|
"in_filter": 1,
|
||||||
"label": "Actual Qty After Transaction",
|
"label": "Qty After Transaction",
|
||||||
"oldfieldname": "bin_aqat",
|
"oldfieldname": "bin_aqat",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"print_width": "150px",
|
"print_width": "150px",
|
||||||
@ -210,7 +210,7 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "stock_value",
|
"fieldname": "stock_value",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Stock Value",
|
"label": "Balance Stock Value",
|
||||||
"oldfieldname": "stock_value",
|
"oldfieldname": "stock_value",
|
||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
@ -219,14 +219,14 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "stock_value_difference",
|
"fieldname": "stock_value_difference",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"label": "Stock Value Difference",
|
"label": "Change in Stock Value",
|
||||||
"options": "Company:company:default_currency",
|
"options": "Company:company:default_currency",
|
||||||
"read_only": 1
|
"read_only": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "stock_queue",
|
"fieldname": "stock_queue",
|
||||||
"fieldtype": "Text",
|
"fieldtype": "Text",
|
||||||
"label": "Stock Queue (FIFO)",
|
"label": "FIFO Stock Queue (qty, rate)",
|
||||||
"oldfieldname": "fcfs_stack",
|
"oldfieldname": "fcfs_stack",
|
||||||
"oldfieldtype": "Text",
|
"oldfieldtype": "Text",
|
||||||
"print_hide": 1,
|
"print_hide": 1,
|
||||||
@ -317,10 +317,11 @@
|
|||||||
"in_create": 1,
|
"in_create": 1,
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-08 13:42:51.857631",
|
"modified": "2021-12-21 06:25:30.040801",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Stock Ledger Entry",
|
"name": "Stock Ledger Entry",
|
||||||
|
"naming_rule": "Expression (old style)",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
@ -338,5 +339,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
@ -1097,7 +1097,7 @@ def apply_price_list(args, as_doc=False):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def apply_price_list_on_item(args):
|
def apply_price_list_on_item(args):
|
||||||
item_doc = frappe.get_doc("Item", args.item_code)
|
item_doc = frappe.db.get_value("Item", args.item_code, ['name', 'variant_of'], as_dict=1)
|
||||||
item_details = get_price_list_rate(args, item_doc)
|
item_details = get_price_list_rate(args, item_doc)
|
||||||
|
|
||||||
item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate))
|
item_details.update(get_pricing_rule_for_item(args, item_details.price_list_rate))
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
|
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
|
from typing import Dict, List, Tuple, Union
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@ -10,19 +11,29 @@ from frappe.utils import cint, date_diff, flt
|
|||||||
|
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|
||||||
|
Filters = frappe._dict
|
||||||
|
|
||||||
def execute(filters=None):
|
def execute(filters: Filters = None) -> Tuple:
|
||||||
columns = get_columns(filters)
|
|
||||||
item_details = get_fifo_queue(filters)
|
|
||||||
to_date = filters["to_date"]
|
to_date = filters["to_date"]
|
||||||
_func = itemgetter(1)
|
columns = get_columns(filters)
|
||||||
|
|
||||||
|
item_details = FIFOSlots(filters).generate()
|
||||||
|
data = format_report_data(filters, item_details, to_date)
|
||||||
|
|
||||||
|
chart_data = get_chart_data(data, filters)
|
||||||
|
|
||||||
|
return columns, data, None, chart_data
|
||||||
|
|
||||||
|
def format_report_data(filters: Filters, item_details: Dict, to_date: str) -> List[Dict]:
|
||||||
|
"Returns ordered, formatted data with ranges."
|
||||||
|
_func = itemgetter(1)
|
||||||
data = []
|
data = []
|
||||||
|
|
||||||
for item, item_dict in item_details.items():
|
for item, item_dict in item_details.items():
|
||||||
earliest_age, latest_age = 0, 0
|
earliest_age, latest_age = 0, 0
|
||||||
|
details = item_dict["details"]
|
||||||
|
|
||||||
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
|
fifo_queue = sorted(filter(_func, item_dict["fifo_queue"]), key=_func)
|
||||||
details = item_dict["details"]
|
|
||||||
|
|
||||||
if not fifo_queue: continue
|
if not fifo_queue: continue
|
||||||
|
|
||||||
@ -31,23 +42,22 @@ def execute(filters=None):
|
|||||||
latest_age = date_diff(to_date, fifo_queue[-1][1])
|
latest_age = date_diff(to_date, fifo_queue[-1][1])
|
||||||
range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict)
|
range1, range2, range3, above_range3 = get_range_age(filters, fifo_queue, to_date, item_dict)
|
||||||
|
|
||||||
row = [details.name, details.item_name,
|
row = [details.name, details.item_name, details.description,
|
||||||
details.description, details.item_group, details.brand]
|
details.item_group, details.brand]
|
||||||
|
|
||||||
if filters.get("show_warehouse_wise_stock"):
|
if filters.get("show_warehouse_wise_stock"):
|
||||||
row.append(details.warehouse)
|
row.append(details.warehouse)
|
||||||
|
|
||||||
row.extend([item_dict.get("total_qty"), average_age,
|
row.extend([item_dict.get("total_qty"), average_age,
|
||||||
range1, range2, range3, above_range3,
|
range1, range2, range3, above_range3,
|
||||||
earliest_age, latest_age, details.stock_uom])
|
earliest_age, latest_age,
|
||||||
|
details.stock_uom])
|
||||||
|
|
||||||
data.append(row)
|
data.append(row)
|
||||||
|
|
||||||
chart_data = get_chart_data(data, filters)
|
return data
|
||||||
|
|
||||||
return columns, data, None, chart_data
|
def get_average_age(fifo_queue: List, to_date: str) -> float:
|
||||||
|
|
||||||
def get_average_age(fifo_queue, to_date):
|
|
||||||
batch_age = age_qty = total_qty = 0.0
|
batch_age = age_qty = total_qty = 0.0
|
||||||
for batch in fifo_queue:
|
for batch in fifo_queue:
|
||||||
batch_age = date_diff(to_date, batch[1])
|
batch_age = date_diff(to_date, batch[1])
|
||||||
@ -61,7 +71,7 @@ def get_average_age(fifo_queue, to_date):
|
|||||||
|
|
||||||
return flt(age_qty / total_qty, 2) if total_qty else 0.0
|
return flt(age_qty / total_qty, 2) if total_qty else 0.0
|
||||||
|
|
||||||
def get_range_age(filters, fifo_queue, to_date, item_dict):
|
def get_range_age(filters: Filters, fifo_queue: List, to_date: str, item_dict: Dict) -> Tuple:
|
||||||
range1 = range2 = range3 = above_range3 = 0.0
|
range1 = range2 = range3 = above_range3 = 0.0
|
||||||
|
|
||||||
for item in fifo_queue:
|
for item in fifo_queue:
|
||||||
@ -79,7 +89,7 @@ def get_range_age(filters, fifo_queue, to_date, item_dict):
|
|||||||
|
|
||||||
return range1, range2, range3, above_range3
|
return range1, range2, range3, above_range3
|
||||||
|
|
||||||
def get_columns(filters):
|
def get_columns(filters: Filters) -> List[Dict]:
|
||||||
range_columns = []
|
range_columns = []
|
||||||
setup_ageing_columns(filters, range_columns)
|
setup_ageing_columns(filters, range_columns)
|
||||||
columns = [
|
columns = [
|
||||||
@ -164,106 +174,7 @@ def get_columns(filters):
|
|||||||
|
|
||||||
return columns
|
return columns
|
||||||
|
|
||||||
def get_fifo_queue(filters, sle=None):
|
def get_chart_data(data: List, filters: Filters) -> Dict:
|
||||||
item_details = {}
|
|
||||||
transferred_item_details = {}
|
|
||||||
serial_no_batch_purchase_details = {}
|
|
||||||
|
|
||||||
if sle == None:
|
|
||||||
sle = get_stock_ledger_entries(filters)
|
|
||||||
|
|
||||||
for d in sle:
|
|
||||||
key = (d.name, d.warehouse) if filters.get('show_warehouse_wise_stock') else d.name
|
|
||||||
item_details.setdefault(key, {"details": d, "fifo_queue": []})
|
|
||||||
fifo_queue = item_details[key]["fifo_queue"]
|
|
||||||
|
|
||||||
transferred_item_key = (d.voucher_no, d.name, d.warehouse)
|
|
||||||
transferred_item_details.setdefault(transferred_item_key, [])
|
|
||||||
|
|
||||||
if d.voucher_type == "Stock Reconciliation":
|
|
||||||
d.actual_qty = flt(d.qty_after_transaction) - flt(item_details[key].get("qty_after_transaction", 0))
|
|
||||||
|
|
||||||
serial_no_list = get_serial_nos(d.serial_no) if d.serial_no else []
|
|
||||||
|
|
||||||
if d.actual_qty > 0:
|
|
||||||
if transferred_item_details.get(transferred_item_key):
|
|
||||||
batch = transferred_item_details[transferred_item_key][0]
|
|
||||||
fifo_queue.append(batch)
|
|
||||||
transferred_item_details[transferred_item_key].pop(0)
|
|
||||||
else:
|
|
||||||
if serial_no_list:
|
|
||||||
for serial_no in serial_no_list:
|
|
||||||
if serial_no_batch_purchase_details.get(serial_no):
|
|
||||||
fifo_queue.append([serial_no, serial_no_batch_purchase_details.get(serial_no)])
|
|
||||||
else:
|
|
||||||
serial_no_batch_purchase_details.setdefault(serial_no, d.posting_date)
|
|
||||||
fifo_queue.append([serial_no, d.posting_date])
|
|
||||||
else:
|
|
||||||
fifo_queue.append([d.actual_qty, d.posting_date])
|
|
||||||
else:
|
|
||||||
if serial_no_list:
|
|
||||||
fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_no_list]
|
|
||||||
else:
|
|
||||||
qty_to_pop = abs(d.actual_qty)
|
|
||||||
while qty_to_pop:
|
|
||||||
batch = fifo_queue[0] if fifo_queue else [0, None]
|
|
||||||
if 0 < flt(batch[0]) <= qty_to_pop:
|
|
||||||
# if batch qty > 0
|
|
||||||
# not enough or exactly same qty in current batch, clear batch
|
|
||||||
qty_to_pop -= flt(batch[0])
|
|
||||||
transferred_item_details[transferred_item_key].append(fifo_queue.pop(0))
|
|
||||||
else:
|
|
||||||
# all from current batch
|
|
||||||
batch[0] = flt(batch[0]) - qty_to_pop
|
|
||||||
transferred_item_details[transferred_item_key].append([qty_to_pop, batch[1]])
|
|
||||||
qty_to_pop = 0
|
|
||||||
|
|
||||||
item_details[key]["qty_after_transaction"] = d.qty_after_transaction
|
|
||||||
|
|
||||||
if "total_qty" not in item_details[key]:
|
|
||||||
item_details[key]["total_qty"] = d.actual_qty
|
|
||||||
else:
|
|
||||||
item_details[key]["total_qty"] += d.actual_qty
|
|
||||||
|
|
||||||
item_details[key]["has_serial_no"] = d.has_serial_no
|
|
||||||
|
|
||||||
return item_details
|
|
||||||
|
|
||||||
def get_stock_ledger_entries(filters):
|
|
||||||
return frappe.db.sql("""select
|
|
||||||
item.name, item.item_name, item_group, brand, description, item.stock_uom, item.has_serial_no,
|
|
||||||
actual_qty, posting_date, voucher_type, voucher_no, serial_no, batch_no, qty_after_transaction, warehouse
|
|
||||||
from `tabStock Ledger Entry` sle,
|
|
||||||
(select name, item_name, description, stock_uom, brand, item_group, has_serial_no
|
|
||||||
from `tabItem` {item_conditions}) item
|
|
||||||
where item_code = item.name and
|
|
||||||
company = %(company)s and
|
|
||||||
posting_date <= %(to_date)s and
|
|
||||||
is_cancelled != 1
|
|
||||||
{sle_conditions}
|
|
||||||
order by posting_date, posting_time, sle.creation, actual_qty""" #nosec
|
|
||||||
.format(item_conditions=get_item_conditions(filters),
|
|
||||||
sle_conditions=get_sle_conditions(filters)), filters, as_dict=True)
|
|
||||||
|
|
||||||
def get_item_conditions(filters):
|
|
||||||
conditions = []
|
|
||||||
if filters.get("item_code"):
|
|
||||||
conditions.append("item_code=%(item_code)s")
|
|
||||||
if filters.get("brand"):
|
|
||||||
conditions.append("brand=%(brand)s")
|
|
||||||
|
|
||||||
return "where {}".format(" and ".join(conditions)) if conditions else ""
|
|
||||||
|
|
||||||
def get_sle_conditions(filters):
|
|
||||||
conditions = []
|
|
||||||
if filters.get("warehouse"):
|
|
||||||
lft, rgt = frappe.db.get_value('Warehouse', filters.get("warehouse"), ['lft', 'rgt'])
|
|
||||||
conditions.append("""warehouse in (select wh.name from `tabWarehouse` wh
|
|
||||||
where wh.lft >= {0} and rgt <= {1})""".format(lft, rgt))
|
|
||||||
|
|
||||||
return "and {}".format(" and ".join(conditions)) if conditions else ""
|
|
||||||
|
|
||||||
def get_chart_data(data, filters):
|
|
||||||
if not data:
|
if not data:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@ -294,17 +205,201 @@ def get_chart_data(data, filters):
|
|||||||
"type" : "bar"
|
"type" : "bar"
|
||||||
}
|
}
|
||||||
|
|
||||||
def setup_ageing_columns(filters, range_columns):
|
def setup_ageing_columns(filters: Filters, range_columns: List):
|
||||||
for i, label in enumerate(["0-{range1}".format(range1=filters["range1"]),
|
ranges = [
|
||||||
"{range1}-{range2}".format(range1=cint(filters["range1"])+ 1, range2=filters["range2"]),
|
f"0 - {filters['range1']}",
|
||||||
"{range2}-{range3}".format(range2=cint(filters["range2"])+ 1, range3=filters["range3"]),
|
f"{cint(filters['range1']) + 1} - {cint(filters['range2'])}",
|
||||||
"{range3}-{above}".format(range3=cint(filters["range3"])+ 1, above=_("Above"))]):
|
f"{cint(filters['range2']) + 1} - {cint(filters['range3'])}",
|
||||||
add_column(range_columns, label="Age ("+ label +")", fieldname='range' + str(i+1))
|
f"{cint(filters['range3']) + 1} - {_('Above')}"
|
||||||
|
]
|
||||||
|
for i, label in enumerate(ranges):
|
||||||
|
fieldname = 'range' + str(i+1)
|
||||||
|
add_column(range_columns, label=f"Age ({label})",fieldname=fieldname)
|
||||||
|
|
||||||
def add_column(range_columns, label, fieldname, fieldtype='Float', width=140):
|
def add_column(range_columns: List, label:str, fieldname: str, fieldtype: str = 'Float', width: int = 140):
|
||||||
range_columns.append(dict(
|
range_columns.append(dict(
|
||||||
label=label,
|
label=label,
|
||||||
fieldname=fieldname,
|
fieldname=fieldname,
|
||||||
fieldtype=fieldtype,
|
fieldtype=fieldtype,
|
||||||
width=width
|
width=width
|
||||||
))
|
))
|
||||||
|
|
||||||
|
|
||||||
|
class FIFOSlots:
|
||||||
|
"Returns FIFO computed slots of inwarded stock as per date."
|
||||||
|
|
||||||
|
def __init__(self, filters: Dict = None , sle: List = None):
|
||||||
|
self.item_details = {}
|
||||||
|
self.transferred_item_details = {}
|
||||||
|
self.serial_no_batch_purchase_details = {}
|
||||||
|
self.filters = filters
|
||||||
|
self.sle = sle
|
||||||
|
|
||||||
|
def generate(self) -> Dict:
|
||||||
|
"""
|
||||||
|
Returns dict of the foll.g structure:
|
||||||
|
Key = Item A / (Item A, Warehouse A)
|
||||||
|
Key: {
|
||||||
|
'details' -> Dict: ** item details **,
|
||||||
|
'fifo_queue' -> List: ** list of lists containing entries/slots for existing stock,
|
||||||
|
consumed/updated and maintained via FIFO. **
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
if self.sle is None:
|
||||||
|
self.sle = self.__get_stock_ledger_entries()
|
||||||
|
|
||||||
|
for d in self.sle:
|
||||||
|
key, fifo_queue, transferred_item_key = self.__init_key_stores(d)
|
||||||
|
|
||||||
|
if d.voucher_type == "Stock Reconciliation":
|
||||||
|
prev_balance_qty = self.item_details[key].get("qty_after_transaction", 0)
|
||||||
|
d.actual_qty = flt(d.qty_after_transaction) - flt(prev_balance_qty)
|
||||||
|
|
||||||
|
serial_nos = get_serial_nos(d.serial_no) if d.serial_no else []
|
||||||
|
|
||||||
|
if d.actual_qty > 0:
|
||||||
|
self.__compute_incoming_stock(d, fifo_queue, transferred_item_key, serial_nos)
|
||||||
|
else:
|
||||||
|
self.__compute_outgoing_stock(d, fifo_queue, transferred_item_key, serial_nos)
|
||||||
|
|
||||||
|
self.__update_balances(d, key)
|
||||||
|
|
||||||
|
return self.item_details
|
||||||
|
|
||||||
|
def __init_key_stores(self, row: Dict) -> Tuple:
|
||||||
|
"Initialise keys and FIFO Queue."
|
||||||
|
|
||||||
|
key = (row.name, row.warehouse) if self.filters.get('show_warehouse_wise_stock') else row.name
|
||||||
|
self.item_details.setdefault(key, {"details": row, "fifo_queue": []})
|
||||||
|
fifo_queue = self.item_details[key]["fifo_queue"]
|
||||||
|
|
||||||
|
transferred_item_key = (row.voucher_no, row.name, row.warehouse)
|
||||||
|
self.transferred_item_details.setdefault(transferred_item_key, [])
|
||||||
|
|
||||||
|
return key, fifo_queue, transferred_item_key
|
||||||
|
|
||||||
|
def __compute_incoming_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List):
|
||||||
|
"Update FIFO Queue on inward stock."
|
||||||
|
|
||||||
|
if self.transferred_item_details.get(transfer_key):
|
||||||
|
# inward/outward from same voucher, item & warehouse
|
||||||
|
slot = self.transferred_item_details[transfer_key].pop(0)
|
||||||
|
fifo_queue.append(slot)
|
||||||
|
else:
|
||||||
|
if not serial_nos:
|
||||||
|
if fifo_queue and flt(fifo_queue[0][0]) < 0:
|
||||||
|
# neutralize negative stock by adding positive stock
|
||||||
|
fifo_queue[0][0] += flt(row.actual_qty)
|
||||||
|
fifo_queue[0][1] = row.posting_date
|
||||||
|
else:
|
||||||
|
fifo_queue.append([flt(row.actual_qty), row.posting_date])
|
||||||
|
return
|
||||||
|
|
||||||
|
for serial_no in serial_nos:
|
||||||
|
if self.serial_no_batch_purchase_details.get(serial_no):
|
||||||
|
fifo_queue.append([serial_no, self.serial_no_batch_purchase_details.get(serial_no)])
|
||||||
|
else:
|
||||||
|
self.serial_no_batch_purchase_details.setdefault(serial_no, row.posting_date)
|
||||||
|
fifo_queue.append([serial_no, row.posting_date])
|
||||||
|
|
||||||
|
def __compute_outgoing_stock(self, row: Dict, fifo_queue: List, transfer_key: Tuple, serial_nos: List):
|
||||||
|
"Update FIFO Queue on outward stock."
|
||||||
|
if serial_nos:
|
||||||
|
fifo_queue[:] = [serial_no for serial_no in fifo_queue if serial_no[0] not in serial_nos]
|
||||||
|
return
|
||||||
|
|
||||||
|
qty_to_pop = abs(row.actual_qty)
|
||||||
|
while qty_to_pop:
|
||||||
|
slot = fifo_queue[0] if fifo_queue else [0, None]
|
||||||
|
if 0 < flt(slot[0]) <= qty_to_pop:
|
||||||
|
# qty to pop >= slot qty
|
||||||
|
# if +ve and not enough or exactly same balance in current slot, consume whole slot
|
||||||
|
qty_to_pop -= flt(slot[0])
|
||||||
|
self.transferred_item_details[transfer_key].append(fifo_queue.pop(0))
|
||||||
|
elif not fifo_queue:
|
||||||
|
# negative stock, no balance but qty yet to consume
|
||||||
|
fifo_queue.append([-(qty_to_pop), row.posting_date])
|
||||||
|
self.transferred_item_details[transfer_key].append([row.actual_qty, row.posting_date])
|
||||||
|
qty_to_pop = 0
|
||||||
|
else:
|
||||||
|
# qty to pop < slot qty, ample balance
|
||||||
|
# consume actual_qty from first slot
|
||||||
|
slot[0] = flt(slot[0]) - qty_to_pop
|
||||||
|
self.transferred_item_details[transfer_key].append([qty_to_pop, slot[1]])
|
||||||
|
qty_to_pop = 0
|
||||||
|
|
||||||
|
def __update_balances(self, row: Dict, key: Union[Tuple, str]):
|
||||||
|
self.item_details[key]["qty_after_transaction"] = row.qty_after_transaction
|
||||||
|
|
||||||
|
if "total_qty" not in self.item_details[key]:
|
||||||
|
self.item_details[key]["total_qty"] = row.actual_qty
|
||||||
|
else:
|
||||||
|
self.item_details[key]["total_qty"] += row.actual_qty
|
||||||
|
|
||||||
|
self.item_details[key]["has_serial_no"] = row.has_serial_no
|
||||||
|
|
||||||
|
def __get_stock_ledger_entries(self) -> List[Dict]:
|
||||||
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
|
item = self.__get_item_query() # used as derived table in sle query
|
||||||
|
|
||||||
|
sle_query = (
|
||||||
|
frappe.qb.from_(sle).from_(item)
|
||||||
|
.select(
|
||||||
|
item.name, item.item_name, item.item_group,
|
||||||
|
item.brand, item.description,
|
||||||
|
item.stock_uom, item.has_serial_no,
|
||||||
|
sle.actual_qty, sle.posting_date,
|
||||||
|
sle.voucher_type, sle.voucher_no,
|
||||||
|
sle.serial_no, sle.batch_no,
|
||||||
|
sle.qty_after_transaction, sle.warehouse
|
||||||
|
).where(
|
||||||
|
(sle.item_code == item.name)
|
||||||
|
& (sle.company == self.filters.get("company"))
|
||||||
|
& (sle.posting_date <= self.filters.get("to_date"))
|
||||||
|
& (sle.is_cancelled != 1)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.filters.get("warehouse"):
|
||||||
|
sle_query = self.__get_warehouse_conditions(sle, sle_query)
|
||||||
|
|
||||||
|
sle_query = sle_query.orderby(
|
||||||
|
sle.posting_date, sle.posting_time, sle.creation, sle.actual_qty
|
||||||
|
)
|
||||||
|
|
||||||
|
return sle_query.run(as_dict=True)
|
||||||
|
|
||||||
|
def __get_item_query(self) -> str:
|
||||||
|
item_table = frappe.qb.DocType("Item")
|
||||||
|
|
||||||
|
item = frappe.qb.from_("Item").select(
|
||||||
|
"name", "item_name", "description", "stock_uom",
|
||||||
|
"brand", "item_group", "has_serial_no"
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.filters.get("item_code"):
|
||||||
|
item = item.where(item_table.item_code == self.filters.get("item_code"))
|
||||||
|
|
||||||
|
if self.filters.get("brand"):
|
||||||
|
item = item.where(item_table.brand == self.filters.get("brand"))
|
||||||
|
|
||||||
|
return item
|
||||||
|
|
||||||
|
def __get_warehouse_conditions(self, sle, sle_query) -> str:
|
||||||
|
warehouse = frappe.qb.DocType("Warehouse")
|
||||||
|
lft, rgt = frappe.db.get_value(
|
||||||
|
"Warehouse",
|
||||||
|
self.filters.get("warehouse"),
|
||||||
|
['lft', 'rgt']
|
||||||
|
)
|
||||||
|
|
||||||
|
warehouse_results = (
|
||||||
|
frappe.qb.from_(warehouse)
|
||||||
|
.select("name").where(
|
||||||
|
(warehouse.lft >= lft)
|
||||||
|
& (warehouse.rgt <= rgt)
|
||||||
|
).run()
|
||||||
|
)
|
||||||
|
warehouse_results = [x[0] for x in warehouse_results]
|
||||||
|
|
||||||
|
return sle_query.where(sle.warehouse.isin(warehouse_results))
|
||||||
|
73
erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
Normal file
73
erpnext/stock/report/stock_ageing/stock_ageing_fifo_logic.md
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
### Concept of FIFO Slots
|
||||||
|
|
||||||
|
Since we need to know age-wise remaining stock, we maintain all the inward entries as slots. So each time stock comes in, a slot is added for the same.
|
||||||
|
|
||||||
|
Eg. For Item A:
|
||||||
|
----------------------
|
||||||
|
Date | Qty | Queue
|
||||||
|
----------------------
|
||||||
|
1st | +50 | [[50, 1-12-2021]]
|
||||||
|
2nd | +20 | [[50, 1-12-2021], [20, 2-12-2021]]
|
||||||
|
----------------------
|
||||||
|
|
||||||
|
Now the queue can tell us the total stock and also how old the stock is.
|
||||||
|
Here, the balance qty is 70.
|
||||||
|
50 qty is (today-the 1st) days old
|
||||||
|
20 qty is (today-the 2nd) days old
|
||||||
|
|
||||||
|
### Calculation of FIFO Slots
|
||||||
|
|
||||||
|
#### Case 1: Outward from sufficient balance qty
|
||||||
|
----------------------
|
||||||
|
Date | Qty | Queue
|
||||||
|
----------------------
|
||||||
|
1st | +50 | [[50, 1-12-2021]]
|
||||||
|
2nd | -20 | [[30, 1-12-2021]]
|
||||||
|
2nd | +20 | [[30, 1-12-2021], [20, 2-12-2021]]
|
||||||
|
|
||||||
|
Here after the first entry, while issuing 20 qty:
|
||||||
|
- **since 20 is lesser than the balance**, **qty_to_pop (20)** is simply consumed from first slot (FIFO consumption)
|
||||||
|
- Any inward entry after as usual will get its own slot added to the queue
|
||||||
|
|
||||||
|
#### Case 2: Outward from sufficient cumulative (slots) balance qty
|
||||||
|
----------------------
|
||||||
|
Date | Qty | Queue
|
||||||
|
----------------------
|
||||||
|
1st | +50 | [[50, 1-12-2021]]
|
||||||
|
2nd | +20 | [[50, 1-12-2021], [20, 2-12-2021]]
|
||||||
|
2nd | -60 | [[10, 2-12-2021]]
|
||||||
|
|
||||||
|
- Consumption happens slot wise. First slot 1 is consumed
|
||||||
|
- Since **qty_to_pop (60) is greater than slot 1 qty (50)**, the entire slot is consumed and popped
|
||||||
|
- Now the queue is [[20, 2-12-2021]], and **qty_to_pop=10** (remaining qty to pop)
|
||||||
|
- It then goes ahead to the next slot and consumes 10 from it
|
||||||
|
- Now the queue is [[10, 2-12-2021]]
|
||||||
|
|
||||||
|
#### Case 3: Outward from insufficient balance qty
|
||||||
|
> This case is possible only if **Allow Negative Stock** was enabled at some point/is enabled.
|
||||||
|
|
||||||
|
----------------------
|
||||||
|
Date | Qty | Queue
|
||||||
|
----------------------
|
||||||
|
1st | +50 | [[50, 1-12-2021]]
|
||||||
|
2nd | -60 | [[-10, 1-12-2021]]
|
||||||
|
|
||||||
|
- Since **qty_to_pop (60)** is more than the balance in slot 1, the entire slot is consumed and popped
|
||||||
|
- Now the queue is **empty**, and **qty_to_pop=10** (remaining qty to pop)
|
||||||
|
- Since we still have more to consume, we append the balance since 60 is issued from 50 i.e. -10.
|
||||||
|
- We register this negative value, since the stock issue has caused the balance to become negative
|
||||||
|
|
||||||
|
Now when stock is inwarded:
|
||||||
|
- Instead of adding a slot we check if there are any negative balances.
|
||||||
|
- If yes, we keep adding positive stock to it until we make the balance positive.
|
||||||
|
- Once the balance is positive, the next inward entry will add a new slot in the queue
|
||||||
|
|
||||||
|
Eg:
|
||||||
|
----------------------
|
||||||
|
Date | Qty | Queue
|
||||||
|
----------------------
|
||||||
|
1st | +50 | [[50, 1-12-2021]]
|
||||||
|
2nd | -60 | [[-10, 1-12-2021]]
|
||||||
|
3rd | +5 | [[-5, 3-12-2021]]
|
||||||
|
4th | +10 | [[5, 4-12-2021]]
|
||||||
|
4th | +20 | [[5, 4-12-2021], [20, 4-12-2021]]
|
126
erpnext/stock/report/stock_ageing/test_stock_ageing.py
Normal file
126
erpnext/stock/report/stock_ageing/test_stock_ageing.py
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
|
# See license.txt
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
|
class TestStockAgeing(ERPNextTestCase):
|
||||||
|
def setUp(self) -> None:
|
||||||
|
self.filters = frappe._dict(
|
||||||
|
company="_Test Company",
|
||||||
|
to_date="2021-12-10"
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_normal_inward_outward_queue(self):
|
||||||
|
"Reference: Case 1 in stock_ageing_fifo_logic.md"
|
||||||
|
sle = [
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=30, qty_after_transaction=30,
|
||||||
|
posting_date="2021-12-01", voucher_type="Stock Entry",
|
||||||
|
voucher_no="001",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
),
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=20, qty_after_transaction=50,
|
||||||
|
posting_date="2021-12-02", voucher_type="Stock Entry",
|
||||||
|
voucher_no="002",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
),
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=(-10), qty_after_transaction=40,
|
||||||
|
posting_date="2021-12-03", voucher_type="Stock Entry",
|
||||||
|
voucher_no="003",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
slots = FIFOSlots(self.filters, sle).generate()
|
||||||
|
|
||||||
|
self.assertTrue(slots["Flask Item"]["fifo_queue"])
|
||||||
|
result = slots["Flask Item"]
|
||||||
|
queue = result["fifo_queue"]
|
||||||
|
|
||||||
|
self.assertEqual(result["qty_after_transaction"], result["total_qty"])
|
||||||
|
self.assertEqual(queue[0][0], 20.0)
|
||||||
|
|
||||||
|
def test_insufficient_balance(self):
|
||||||
|
"Reference: Case 3 in stock_ageing_fifo_logic.md"
|
||||||
|
sle = [
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=(-30), qty_after_transaction=(-30),
|
||||||
|
posting_date="2021-12-01", voucher_type="Stock Entry",
|
||||||
|
voucher_no="001",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
),
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=20, qty_after_transaction=(-10),
|
||||||
|
posting_date="2021-12-02", voucher_type="Stock Entry",
|
||||||
|
voucher_no="002",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
),
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=20, qty_after_transaction=10,
|
||||||
|
posting_date="2021-12-03", voucher_type="Stock Entry",
|
||||||
|
voucher_no="003",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
),
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=10, qty_after_transaction=20,
|
||||||
|
posting_date="2021-12-03", voucher_type="Stock Entry",
|
||||||
|
voucher_no="004",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
slots = FIFOSlots(self.filters, sle).generate()
|
||||||
|
|
||||||
|
result = slots["Flask Item"]
|
||||||
|
queue = result["fifo_queue"]
|
||||||
|
|
||||||
|
self.assertEqual(result["qty_after_transaction"], result["total_qty"])
|
||||||
|
self.assertEqual(queue[0][0], 10.0)
|
||||||
|
self.assertEqual(queue[1][0], 10.0)
|
||||||
|
|
||||||
|
def test_stock_reconciliation(self):
|
||||||
|
sle = [
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=30, qty_after_transaction=30,
|
||||||
|
posting_date="2021-12-01", voucher_type="Stock Entry",
|
||||||
|
voucher_no="001",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
),
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=0, qty_after_transaction=50,
|
||||||
|
posting_date="2021-12-02", voucher_type="Stock Reconciliation",
|
||||||
|
voucher_no="002",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
),
|
||||||
|
frappe._dict(
|
||||||
|
name="Flask Item",
|
||||||
|
actual_qty=(-10), qty_after_transaction=40,
|
||||||
|
posting_date="2021-12-03", voucher_type="Stock Entry",
|
||||||
|
voucher_no="003",
|
||||||
|
has_serial_no=False, serial_no=None
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
slots = FIFOSlots(self.filters, sle).generate()
|
||||||
|
|
||||||
|
result = slots["Flask Item"]
|
||||||
|
queue = result["fifo_queue"]
|
||||||
|
|
||||||
|
self.assertEqual(result["qty_after_transaction"], result["total_qty"])
|
||||||
|
self.assertEqual(queue[0][0], 20.0)
|
||||||
|
self.assertEqual(queue[1][0], 20.0)
|
@ -9,7 +9,7 @@ from frappe import _
|
|||||||
from frappe.utils import cint, date_diff, flt, getdate
|
from frappe.utils import cint, date_diff, flt, getdate
|
||||||
|
|
||||||
import erpnext
|
import erpnext
|
||||||
from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, get_fifo_queue
|
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
|
||||||
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
|
||||||
from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress
|
from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress
|
||||||
|
|
||||||
@ -33,7 +33,7 @@ def execute(filters=None):
|
|||||||
|
|
||||||
if filters.get('show_stock_ageing_data'):
|
if filters.get('show_stock_ageing_data'):
|
||||||
filters['show_warehouse_wise_stock'] = True
|
filters['show_warehouse_wise_stock'] = True
|
||||||
item_wise_fifo_queue = get_fifo_queue(filters, sle)
|
item_wise_fifo_queue = FIFOSlots(filters, sle).generate()
|
||||||
|
|
||||||
# if no stock ledger entry found return
|
# if no stock ledger entry found return
|
||||||
if not sle:
|
if not sle:
|
||||||
|
@ -8,7 +8,8 @@ const DIFFERNCE_FIELD_NAMES = [
|
|||||||
"fifo_value_diff",
|
"fifo_value_diff",
|
||||||
"fifo_valuation_diff",
|
"fifo_valuation_diff",
|
||||||
"valuation_diff",
|
"valuation_diff",
|
||||||
"fifo_difference_diff"
|
"fifo_difference_diff",
|
||||||
|
"diff_value_diff"
|
||||||
];
|
];
|
||||||
|
|
||||||
frappe.query_reports["Stock Ledger Invariant Check"] = {
|
frappe.query_reports["Stock Ledger Invariant Check"] = {
|
||||||
|
@ -50,6 +50,7 @@ def get_stock_ledger_entries(filters):
|
|||||||
|
|
||||||
def add_invariant_check_fields(sles):
|
def add_invariant_check_fields(sles):
|
||||||
balance_qty = 0.0
|
balance_qty = 0.0
|
||||||
|
balance_stock_value = 0.0
|
||||||
for idx, sle in enumerate(sles):
|
for idx, sle in enumerate(sles):
|
||||||
queue = json.loads(sle.stock_queue)
|
queue = json.loads(sle.stock_queue)
|
||||||
|
|
||||||
@ -60,6 +61,7 @@ def add_invariant_check_fields(sles):
|
|||||||
fifo_value += qty * rate
|
fifo_value += qty * rate
|
||||||
|
|
||||||
balance_qty += sle.actual_qty
|
balance_qty += sle.actual_qty
|
||||||
|
balance_stock_value += sle.stock_value_difference
|
||||||
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no:
|
||||||
balance_qty = sle.qty_after_transaction
|
balance_qty = sle.qty_after_transaction
|
||||||
|
|
||||||
@ -70,6 +72,7 @@ def add_invariant_check_fields(sles):
|
|||||||
sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
|
sle.stock_value / sle.qty_after_transaction if sle.qty_after_transaction else None
|
||||||
)
|
)
|
||||||
sle.expected_qty_after_transaction = balance_qty
|
sle.expected_qty_after_transaction = balance_qty
|
||||||
|
sle.stock_value_from_diff = balance_stock_value
|
||||||
|
|
||||||
# set difference fields
|
# set difference fields
|
||||||
sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction
|
sle.difference_in_qty = sle.qty_after_transaction - sle.expected_qty_after_transaction
|
||||||
@ -81,6 +84,7 @@ def add_invariant_check_fields(sles):
|
|||||||
sle.valuation_diff = (
|
sle.valuation_diff = (
|
||||||
sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
|
sle.valuation_rate - sle.balance_value_by_qty if sle.balance_value_by_qty else None
|
||||||
)
|
)
|
||||||
|
sle.diff_value_diff = sle.stock_value_from_diff - sle.stock_value
|
||||||
|
|
||||||
if idx > 0:
|
if idx > 0:
|
||||||
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
|
sle.fifo_stock_diff = sle.fifo_stock_value - sles[idx - 1].fifo_stock_value
|
||||||
@ -191,12 +195,21 @@ def get_columns():
|
|||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "D - E",
|
"label": "D - E",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
"fieldname": "stock_value_difference",
|
"fieldname": "stock_value_difference",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"label": "(F) Stock Value Difference",
|
"label": "(F) Stock Value Difference",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "stock_value_from_diff",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "Balance Stock Value using (F)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "diff_value_diff",
|
||||||
|
"fieldtype": "Float",
|
||||||
|
"label": "K - D",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "fifo_stock_diff",
|
"fieldname": "fifo_stock_diff",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
|
@ -9,7 +9,7 @@ import frappe
|
|||||||
from frappe import _
|
from frappe import _
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
|
|
||||||
from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, get_fifo_queue
|
from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age
|
||||||
from erpnext.stock.report.stock_balance.stock_balance import (
|
from erpnext.stock.report.stock_balance.stock_balance import (
|
||||||
get_item_details,
|
get_item_details,
|
||||||
get_item_warehouse_map,
|
get_item_warehouse_map,
|
||||||
@ -33,7 +33,7 @@ def execute(filters=None):
|
|||||||
item_map = get_item_details(items, sle, filters)
|
item_map = get_item_details(items, sle, filters)
|
||||||
iwb_map = get_item_warehouse_map(filters, sle)
|
iwb_map = get_item_warehouse_map(filters, sle)
|
||||||
warehouse_list = get_warehouse_list(filters)
|
warehouse_list = get_warehouse_list(filters)
|
||||||
item_ageing = get_fifo_queue(filters)
|
item_ageing = FIFOSlots(filters).generate()
|
||||||
data = []
|
data = []
|
||||||
item_balance = {}
|
item_balance = {}
|
||||||
item_value = {}
|
item_value = {}
|
||||||
|
@ -98,6 +98,7 @@ class TestIssue(TestSetUp):
|
|||||||
issue.save()
|
issue.save()
|
||||||
|
|
||||||
self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
|
self.assertEqual(issue.on_hold_since, frappe.flags.current_time)
|
||||||
|
self.assertFalse(issue.resolution_by)
|
||||||
|
|
||||||
creation = get_datetime("2020-03-04 5:00")
|
creation = get_datetime("2020-03-04 5:00")
|
||||||
frappe.flags.current_time = get_datetime("2020-03-04 5:00")
|
frappe.flags.current_time = get_datetime("2020-03-04 5:00")
|
||||||
|
@ -476,7 +476,7 @@ def update_response_and_resolution_metrics(doc, apply_sla_for_resolution):
|
|||||||
priority = get_response_and_resolution_duration(doc)
|
priority = get_response_and_resolution_duration(doc)
|
||||||
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
|
start_date_time = get_datetime(doc.get("service_level_agreement_creation") or doc.creation)
|
||||||
set_response_by(doc, start_date_time, priority)
|
set_response_by(doc, start_date_time, priority)
|
||||||
if apply_sla_for_resolution:
|
if apply_sla_for_resolution and not doc.get('on_hold_since'): # resolution_by is reset if on hold
|
||||||
set_resolution_by(doc, start_date_time, priority)
|
set_resolution_by(doc, start_date_time, priority)
|
||||||
|
|
||||||
|
|
||||||
@ -624,9 +624,6 @@ def reset_resolution_metrics(doc):
|
|||||||
if doc.meta.has_field("user_resolution_time"):
|
if doc.meta.has_field("user_resolution_time"):
|
||||||
doc.user_resolution_time = None
|
doc.user_resolution_time = None
|
||||||
|
|
||||||
if doc.meta.has_field("agreement_status"):
|
|
||||||
doc.agreement_status = "First Response Due"
|
|
||||||
|
|
||||||
|
|
||||||
# called via hooks on communication update
|
# called via hooks on communication update
|
||||||
def on_communication_update(doc, status):
|
def on_communication_update(doc, status):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user