Merge branch 'develop' into separate-discount-account

This commit is contained in:
rahib-hassan
2022-04-25 12:17:50 +05:30
committed by GitHub
30 changed files with 1421 additions and 1323 deletions

View File

@@ -5,7 +5,10 @@
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, fmt_money, getdate, nowdate from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate
import erpnext
form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"} form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
@@ -76,6 +79,52 @@ class BankClearance(Document):
as_dict=1, as_dict=1,
) )
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
loan_disbursements = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document"),
loan_disbursement.name.as_("payment_entry"),
loan_disbursement.disbursed_amount.as_("credit"),
ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, frappe.qb.desc)
).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
loan_repayments = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document"),
loan_repayment.name.as_("payment_entry"),
loan_repayment.amount_paid.as_("debit"),
ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"),
loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
.orderby(loan_repayment.posting_date)
.orderby(loan_repayment.name, frappe.qb.desc)
).run(as_dict=1)
pos_sales_invoices, pos_purchase_invoices = [], [] pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions: if self.include_pos_transactions:
pos_sales_invoices = frappe.db.sql( pos_sales_invoices = frappe.db.sql(
@@ -114,20 +163,29 @@ class BankClearance(Document):
entries = sorted( entries = sorted(
list(payment_entries) list(payment_entries)
+ list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)), + list(journal_entries)
key=lambda k: k["posting_date"] or getdate(nowdate()), + list(pos_sales_invoices)
+ list(pos_purchase_invoices)
+ list(loan_disbursements)
+ list(loan_repayments),
key=lambda k: getdate(k["posting_date"]),
) )
self.set("payment_entries", []) self.set("payment_entries", [])
self.total_amount = 0.0 self.total_amount = 0.0
default_currency = erpnext.get_default_currency()
for d in entries: for d in entries:
row = self.append("payment_entries", {}) row = self.append("payment_entries", {})
amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0)) amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
if not d.get("account_currency"):
d.account_currency = default_currency
formatted_amount = fmt_money(abs(amount), 2, d.account_currency) formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr")) d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
d.posting_date = getdate(d.posting_date)
d.pop("credit") d.pop("credit")
d.pop("debit") d.pop("debit")

View File

@@ -1,9 +1,96 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
# import frappe
import unittest import unittest
import frappe
from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_accounts,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
class TestBankClearance(unittest.TestCase): class TestBankClearance(unittest.TestCase):
pass @classmethod
def setUpClass(cls):
make_bank_account()
create_loan_accounts()
create_loan_masters()
add_transactions()
# Basic test case to test if bank clearance tool doesn't break
# Detailed test can be added later
def test_bank_clearance(self):
bank_clearance = frappe.get_doc("Bank Clearance")
bank_clearance.account = "_Test Bank Clearance - _TC"
bank_clearance.from_date = add_months(getdate(), -1)
bank_clearance.to_date = getdate()
bank_clearance.get_payment_entries()
self.assertEqual(len(bank_clearance.payment_entries), 3)
def make_bank_account():
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
frappe.get_doc(
{
"doctype": "Account",
"account_type": "Bank",
"account_name": "_Test Bank Clearance",
"company": "_Test Company",
"parent_account": "Bank Accounts - _TC",
}
).insert()
def create_loan_masters():
create_loan_type(
"Clearance Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"_Test Bank Clearance - _TC",
"_Test Bank Clearance - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
)
def add_transactions():
make_payment_entry()
make_loan()
def make_loan():
loan = create_loan(
"_Test Customer",
"Clearance Loan",
280000,
"Repay Over Number of Periods",
20,
applicant_type="Customer",
)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
repayment_entry.save()
repayment_entry.submit()
def make_payment_entry():
pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
pe.insert()
pe.submit()

View File

@@ -1,10 +1,17 @@
import unittest import unittest
import frappe
from frappe.test_runner import make_test_objects from frappe.test_runner import make_test_objects
from erpnext.accounts.party import get_party_shipping_address from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries from erpnext.accounts.utils import (
get_future_stock_vouchers,
get_voucherwise_gl_entries,
sort_stock_vouchers_by_posting_date,
)
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
class TestUtils(unittest.TestCase): class TestUtils(unittest.TestCase):
@@ -47,6 +54,25 @@ class TestUtils(unittest.TestCase):
msg="get_voucherwise_gl_entries not returning expected GLes", msg="get_voucherwise_gl_entries not returning expected GLes",
) )
def test_stock_voucher_sorting(self):
vouchers = []
item = make_item().name
stock_entry = {"item": item, "to_warehouse": "_Test Warehouse - _TC", "qty": 1, "rate": 10}
se1 = make_stock_entry(posting_date="2022-01-01", **stock_entry)
se2 = make_stock_entry(posting_date="2022-02-01", **stock_entry)
se3 = make_stock_entry(posting_date="2022-03-01", **stock_entry)
for doc in (se1, se2, se3):
vouchers.append((doc.doctype, doc.name))
vouchers.append(("Stock Entry", "Wat"))
sorted_vouchers = sort_stock_vouchers_by_posting_date(list(reversed(vouchers)))
self.assertEqual(sorted_vouchers, vouchers)
ADDRESS_RECORDS = [ ADDRESS_RECORDS = [
{ {

View File

@@ -3,6 +3,7 @@
from json import loads from json import loads
from typing import List, Tuple
import frappe import frappe
import frappe.defaults import frappe.defaults
@@ -1122,6 +1123,9 @@ def update_gl_entries_after(
def repost_gle_for_stock_vouchers( def repost_gle_for_stock_vouchers(
stock_vouchers, posting_date, company=None, warehouse_account=None stock_vouchers, posting_date, company=None, warehouse_account=None
): ):
if not stock_vouchers:
return
def _delete_gl_entries(voucher_type, voucher_no): def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql( frappe.db.sql(
"""delete from `tabGL Entry` """delete from `tabGL Entry`
@@ -1129,6 +1133,8 @@ def repost_gle_for_stock_vouchers(
(voucher_type, voucher_no), (voucher_type, voucher_no),
) )
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if not warehouse_account: if not warehouse_account:
warehouse_account = get_warehouse_account_map(company) warehouse_account = get_warehouse_account_map(company)
@@ -1149,6 +1155,27 @@ def repost_gle_for_stock_vouchers(
_delete_gl_entries(voucher_type, voucher_no) _delete_gl_entries(voucher_type, voucher_no)
def sort_stock_vouchers_by_posting_date(
stock_vouchers: List[Tuple[str, str]]
) -> List[Tuple[str, str]]:
sle = frappe.qb.DocType("Stock Ledger Entry")
voucher_nos = [v[1] for v in stock_vouchers]
sles = (
frappe.qb.from_(sle)
.select(sle.voucher_type, sle.voucher_no, sle.posting_date, sle.posting_time, sle.creation)
.where((sle.is_cancelled == 0) & (sle.voucher_no.isin(voucher_nos)))
.groupby(sle.voucher_type, sle.voucher_no)
).run(as_dict=True)
sorted_vouchers = [(sle.voucher_type, sle.voucher_no) for sle in sles]
unknown_vouchers = set(stock_vouchers) - set(sorted_vouchers)
if unknown_vouchers:
sorted_vouchers.extend(unknown_vouchers)
return sorted_vouchers
def get_future_stock_vouchers( def get_future_stock_vouchers(
posting_date, posting_time, for_warehouses=None, for_items=None, company=None posting_date, posting_time, for_warehouses=None, for_items=None, company=None
): ):

View File

@@ -62,6 +62,8 @@
"holiday_list", "holiday_list",
"default_shift", "default_shift",
"salary_information", "salary_information",
"salary_currency",
"ctc",
"salary_mode", "salary_mode",
"payroll_cost_center", "payroll_cost_center",
"column_break_52", "column_break_52",
@@ -807,13 +809,25 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Shift Request Approver", "label": "Shift Request Approver",
"options": "User" "options": "User"
},
{
"fieldname": "salary_currency",
"fieldtype": "Link",
"label": "Salary Currency",
"options": "Currency"
},
{
"fieldname": "ctc",
"fieldtype": "Currency",
"label": "Cost to Company (CTC)",
"options": "salary_currency"
} }
], ],
"icon": "fa fa-user", "icon": "fa fa-user",
"idx": 24, "idx": 24,
"image_field": "image", "image_field": "image",
"links": [], "links": [],
"modified": "2022-03-22 13:44:37.088519", "modified": "2022-04-22 16:21:55.811983",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee", "name": "Employee",

View File

@@ -1,365 +1,148 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "HR-EMP-PRO-.YYYY.-.#####", "autoname": "HR-EMP-PRO-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 18:33:59.476562", "creation": "2018-04-13 18:33:59.476562",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"employee",
"employee_name",
"department",
"salary_currency",
"column_break_3",
"promotion_date",
"company",
"details_section",
"promotion_details",
"salary_details_section",
"current_ctc",
"column_break_12",
"revised_ctc",
"amended_from"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "employee", "fieldname": "employee",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Employee", "label": "Employee",
"length": 0,
"no_copy": 0,
"options": "Employee", "options": "Employee",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.employee_name", "fetch_from": "employee.employee_name",
"fieldname": "employee_name", "fieldname": "employee_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee Name", "label": "Employee Name",
"length": 0, "read_only": 1
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.department", "fetch_from": "employee.department",
"fieldname": "department", "fieldname": "department",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Department", "label": "Department",
"length": 0,
"no_copy": 0,
"options": "Department", "options": "Department",
"permlevel": 0, "read_only": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "promotion_date", "fieldname": "promotion_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Promotion Date", "label": "Promotion Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "employee.company", "fetch_from": "employee.company",
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Company", "label": "Company",
"length": 0, "options": "Company"
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "description": "Set the properties that should be updated in the Employee master on promotion submission",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "details_section", "fieldname": "details_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Employee Promotion Details"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee Promotion Details",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "promotion_details", "fieldname": "promotion_details",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Employee Promotion Detail",
"length": 0,
"no_copy": 0,
"options": "Employee Property History", "options": "Employee Property History",
"permlevel": 0, "reqd": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Amended From", "label": "Amended From",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "Employee Promotion", "options": "Employee Promotion",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "read_only": 1
"read_only": 1, },
"remember_last_selected_value": 0, {
"report_hide": 0, "fieldname": "salary_details_section",
"reqd": 0, "fieldtype": "Section Break",
"search_index": 0, "label": "Salary Details"
"set_only_once": 0, },
"translatable": 0, {
"unique": 0 "fieldname": "column_break_12",
"fieldtype": "Column Break"
},
{
"fetch_from": "employee.salary_currency",
"fieldname": "salary_currency",
"fieldtype": "Link",
"label": "Salary Currency",
"options": "Currency",
"read_only": 1
},
{
"fetch_from": "employee.ctc",
"fetch_if_empty": 1,
"fieldname": "current_ctc",
"fieldtype": "Currency",
"label": "Current CTC",
"mandatory_depends_on": "revised_ctc",
"options": "salary_currency"
},
{
"depends_on": "current_ctc",
"fieldname": "revised_ctc",
"fieldtype": "Currency",
"label": "Revised CTC",
"options": "salary_currency"
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1, "is_submittable": 1,
"issingle": 0, "links": [],
"istable": 0, "modified": "2022-04-22 18:47:10.168744",
"max_attachments": 0,
"modified": "2018-08-21 16:15:40.284987",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Employee Promotion", "name": "Employee Promotion",
"name_case": "", "naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Employee", "role": "Employee",
"set_user_permissions": 0, "share": 1
"share": 1,
"submit": 0,
"write": 0
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 0,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR User", "role": "HR User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
@@ -371,27 +154,19 @@
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 1, "submit": 1,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "employee_name", "title_field": "employee_name",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@@ -26,9 +26,17 @@ class EmployeePromotion(Document):
employee = update_employee_work_history( employee = update_employee_work_history(
employee, self.promotion_details, date=self.promotion_date employee, self.promotion_details, date=self.promotion_date
) )
if self.revised_ctc:
employee.ctc = self.revised_ctc
employee.save() employee.save()
def on_cancel(self): def on_cancel(self):
employee = frappe.get_doc("Employee", self.employee) employee = frappe.get_doc("Employee", self.employee)
employee = update_employee_work_history(employee, self.promotion_details, cancel=True) employee = update_employee_work_history(employee, self.promotion_details, cancel=True)
if self.revised_ctc:
employee.ctc = self.current_ctc
employee.save() employee.save()

View File

@@ -4,21 +4,22 @@
import unittest import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate from frappe.utils import add_days, getdate
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee
class TestEmployeePromotion(unittest.TestCase): class TestEmployeePromotion(FrappeTestCase):
def setUp(self): def setUp(self):
self.employee = make_employee("employee@promotions.com") frappe.db.delete("Employee Promotion")
frappe.db.sql("""delete from `tabEmployee Promotion`""")
def test_submit_before_promotion_date(self): def test_submit_before_promotion_date(self):
promotion_obj = frappe.get_doc( employee = make_employee("employee@promotions.com")
promotion = frappe.get_doc(
{ {
"doctype": "Employee Promotion", "doctype": "Employee Promotion",
"employee": self.employee, "employee": employee,
"promotion_details": [ "promotion_details": [
{ {
"property": "Designation", "property": "Designation",
@@ -29,10 +30,68 @@ class TestEmployeePromotion(unittest.TestCase):
], ],
} }
) )
promotion_obj.promotion_date = add_days(getdate(), 1) promotion.promotion_date = add_days(getdate(), 1)
promotion_obj.save() self.assertRaises(frappe.DocstatusTransitionError, promotion.submit)
self.assertRaises(frappe.DocstatusTransitionError, promotion_obj.submit)
promotion = frappe.get_doc("Employee Promotion", promotion_obj.name)
promotion.promotion_date = getdate() promotion.promotion_date = getdate()
promotion.submit() promotion.submit()
self.assertEqual(promotion.docstatus, 1) self.assertEqual(promotion.docstatus, 1)
def test_employee_history(self):
for grade in ["L1", "L2"]:
frappe.get_doc({"doctype": "Employee Grade", "__newname": grade}).insert()
employee = make_employee(
"test_employee_promotion@example.com",
company="_Test Company",
date_of_birth=getdate("30-09-1980"),
date_of_joining=getdate("01-10-2021"),
designation="Software Developer",
grade="L1",
salary_currency="INR",
ctc="500000",
)
promotion = frappe.get_doc(
{
"doctype": "Employee Promotion",
"employee": employee,
"promotion_date": getdate(),
"revised_ctc": "1000000",
"promotion_details": [
{
"property": "Designation",
"current": "Software Developer",
"new": "Project Manager",
"fieldname": "designation",
},
{"property": "Grade", "current": "L1", "new": "L2", "fieldname": "grade"},
],
}
).submit()
# employee fields updated
employee = frappe.get_doc("Employee", employee)
self.assertEqual(employee.grade, "L2")
self.assertEqual(employee.designation, "Project Manager")
self.assertEqual(employee.ctc, 1000000)
# internal work history updated
self.assertEqual(employee.internal_work_history[0].designation, "Software Developer")
self.assertEqual(employee.internal_work_history[0].from_date, getdate("01-10-2021"))
self.assertEqual(employee.internal_work_history[1].designation, "Project Manager")
self.assertEqual(employee.internal_work_history[1].from_date, getdate())
promotion.cancel()
employee.reload()
# fields restored
self.assertEqual(employee.grade, "L1")
self.assertEqual(employee.designation, "Software Developer")
self.assertEqual(employee.ctc, 500000)
# internal work history updated on cancellation
self.assertEqual(len(employee.internal_work_history), 1)
self.assertEqual(employee.internal_work_history[0].designation, "Software Developer")
self.assertEqual(employee.internal_work_history[0].from_date, getdate("01-10-2021"))

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Employee Transfer Property', {
refresh: function(frm) {
}
});

View File

@@ -1,154 +0,0 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-04-13 18:24:30.579965",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "property",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Property",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "current",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "new",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "New",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-04-13 18:25:54.889579",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Transfer Property",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

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

View File

@@ -1,8 +0,0 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestEmployeeTransferProperty(unittest.TestCase):
pass

View File

@@ -8,39 +8,65 @@ frappe.ui.form.on(cur_frm.doctype, {
}; };
}); });
}, },
onload: function(frm){
if(frm.doc.__islocal){ onload: function(frm) {
if(frm.doctype == "Employee Promotion"){ if (frm.doc.__islocal)
frm.doc.promotion_details = []; frm.trigger("clear_property_table");
}else if (frm.doctype == "Employee Transfer") {
frm.doc.transfer_details = [];
}
}
}, },
employee: function(frm) { employee: function(frm) {
frm.add_fetch("employee", "company", "company"); frm.trigger("clear_property_table");
}, },
clear_property_table: function(frm) {
let table = (frm.doctype == "Employee Promotion") ? "promotion_details" : "transfer_details";
frm.clear_table(table);
frm.refresh_field(table);
frm.fields_dict[table].grid.wrapper.find(".grid-add-row").hide();
},
refresh: function(frm) { refresh: function(frm) {
var table; let table;
if(frm.doctype == "Employee Promotion"){ if (frm.doctype == "Employee Promotion") {
table = "promotion_details"; table = "promotion_details";
}else if (frm.doctype == "Employee Transfer") { } else if (frm.doctype == "Employee Transfer") {
table = "transfer_details"; table = "transfer_details";
} }
if(!table){return;}
cur_frm.fields_dict[table].grid.wrapper.find('.grid-add-row').hide(); if (!table)
cur_frm.fields_dict[table].grid.add_custom_button(__('Add Row'), () => { return;
if(!frm.doc.employee){
frappe.msgprint(__("Please select Employee")); frm.fields_dict[table].grid.wrapper.find(".grid-add-row").hide();
frm.events.setup_employee_property_button(frm, table);
},
setup_employee_property_button: function(frm, table) {
frm.fields_dict[table].grid.add_custom_button(__("Add Employee Property"), () => {
if (!frm.doc.employee) {
frappe.msgprint(__("Please select Employee first."));
return; return;
} }
frappe.call({
method: 'erpnext.hr.utils.get_employee_fields_label', const allowed_fields = [];
callback: function(r) { const exclude_fields = ["naming_series", "employee", "first_name", "middle_name", "last_name", "marital_status", "ctc",
if(r.message){ "employee_name", "status", "image", "gender", "date_of_birth", "date_of_joining", "lft", "rgt", "old_parent"];
show_dialog(frm, table, r.message);
const exclude_field_types = ["HTML", "Section Break", "Column Break", "Button", "Read Only", "Tab Break", "Table"];
frappe.model.with_doctype("Employee", () => {
const field_label_map = {};
frappe.get_meta("Employee").fields.forEach(d => {
field_label_map[d.fieldname] = __(d.label) + ` (${d.fieldname})`;
if (!in_list(exclude_field_types, d.fieldtype) && !in_list(exclude_fields, d.fieldname)) {
allowed_fields.push({
label: field_label_map[d.fieldname],
value: d.fieldname,
});
} }
} });
show_dialog(frm, table, allowed_fields);
}); });
}); });
} }
@@ -50,21 +76,20 @@ var show_dialog = function(frm, table, field_labels) {
var d = new frappe.ui.Dialog({ var d = new frappe.ui.Dialog({
title: "Update Property", title: "Update Property",
fields: [ fields: [
{fieldname: "property", label: __('Select Property'), fieldtype:"Select", options: field_labels}, {fieldname: "property", label: __("Select Property"), fieldtype: "Autocomplete", options: field_labels},
{fieldname: "current", fieldtype: "Data", label:__('Current'), read_only: true}, {fieldname: "current", fieldtype: "Data", label: __("Current"), read_only: true},
{fieldname: "field_html", fieldtype: "HTML"} {fieldname: "new_value", fieldtype: "Data", label: __("New")}
], ],
primary_action_label: __('Add to Details'), primary_action_label: __("Add to Details"),
primary_action: () => { primary_action: () => {
d.get_primary_btn().attr('disabled', true); d.get_primary_btn().attr("disabled", true);
if(d.data) { if (d.data) {
var input = $('[data-fieldname="field_html"] input'); d.data.new = d.get_values().new_value;
d.data.new = input.val();
$(input).remove();
add_to_details(frm, d, table); add_to_details(frm, d, table);
} }
} }
}); });
d.fields_dict["property"].df.onchange = () => { d.fields_dict["property"].df.onchange = () => {
let property = d.get_values().property; let property = d.get_values().property;
d.data.fieldname = property; d.data.fieldname = property;
@@ -73,10 +98,10 @@ var show_dialog = function(frm, table, field_labels) {
method: 'erpnext.hr.utils.get_employee_field_property', method: 'erpnext.hr.utils.get_employee_field_property',
args: {employee: frm.doc.employee, fieldname: property}, args: {employee: frm.doc.employee, fieldname: property},
callback: function(r) { callback: function(r) {
if(r.message){ if (r.message) {
d.data.current = r.message.value; d.data.current = r.message.value;
d.data.property = r.message.label; d.data.property = r.message.label;
d.fields_dict.field_html.$wrapper.html("");
d.set_value('current', r.message.value); d.set_value('current', r.message.value);
render_dynamic_field(d, r.message.datatype, r.message.options, property); render_dynamic_field(d, r.message.datatype, r.message.options, property);
d.get_primary_btn().attr('disabled', false); d.get_primary_btn().attr('disabled', false);
@@ -95,25 +120,26 @@ var render_dynamic_field = function(d, fieldtype, options, fieldname) {
df: { df: {
"fieldtype": fieldtype, "fieldtype": fieldtype,
"fieldname": fieldname, "fieldname": fieldname,
"options": options || '' "options": options || '',
"label": __("New")
}, },
parent: d.fields_dict.field_html.wrapper, parent: d.fields_dict.new_value.wrapper,
only_input: false only_input: false
}); });
dynamic_field.make_input(); dynamic_field.make_input();
$(dynamic_field.label_area).text(__("New")); d.replace_field("new_value", dynamic_field.df);
}; };
var add_to_details = function(frm, d, table) { var add_to_details = function(frm, d, table) {
let data = d.data; let data = d.data;
if(data.fieldname){ if (data.fieldname) {
if(validate_duplicate(frm, table, data.fieldname)){ if (validate_duplicate(frm, table, data.fieldname)) {
frappe.show_alert({message:__("Property already added"), indicator:'orange'}); frappe.show_alert({message: __("Property already added"), indicator: "orange"});
return false; return false;
} }
if(data.current == data.new){ if (data.current == data.new) {
frappe.show_alert({message:__("Nothing to change"), indicator:'orange'}); frappe.show_alert({message: __("Nothing to change"), indicator: "orange"});
d.get_primary_btn().attr('disabled', false); d.get_primary_btn().attr("disabled", false);
return false; return false;
} }
frm.add_child(table, { frm.add_child(table, {
@@ -123,13 +149,16 @@ var add_to_details = function(frm, d, table) {
new: data.new new: data.new
}); });
frm.refresh_field(table); frm.refresh_field(table);
d.fields_dict.field_html.$wrapper.html("");
frm.fields_dict[table].grid.wrapper.find(".grid-add-row").hide();
d.fields_dict.new_value.$wrapper.html("");
d.set_value("property", ""); d.set_value("property", "");
d.set_value('current', ""); d.set_value("current", "");
frappe.show_alert({message:__("Added to details"),indicator:'green'}); frappe.show_alert({message: __("Added to details"), indicator: "green"});
d.data = {}; d.data = {};
}else { } else {
frappe.show_alert({message:__("Value missing"),indicator:'red'}); frappe.show_alert({message: __("Value missing"), indicator: "red"});
} }
}; };

View File

@@ -88,29 +88,6 @@ def delete_employee_work_history(details, employee, date):
frappe.db.delete("Employee Internal Work History", filters) frappe.db.delete("Employee Internal Work History", filters)
@frappe.whitelist()
def get_employee_fields_label():
fields = []
for df in frappe.get_meta("Employee").get("fields"):
if df.fieldname in [
"salutation",
"user_id",
"employee_number",
"employment_type",
"holiday_list",
"branch",
"department",
"designation",
"grade",
"notice_number_of_days",
"reports_to",
"leave_policy",
"company_email",
]:
fields.append({"value": df.fieldname, "label": df.label})
return fields
@frappe.whitelist() @frappe.whitelist()
def get_employee_field_property(employee, fieldname): def get_employee_field_property(employee, fieldname):
if employee and fieldname: if employee and fieldname:

View File

@@ -366,3 +366,4 @@ erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances
erpnext.patches.v13_0.create_gst_custom_fields_in_quotation erpnext.patches.v13_0.create_gst_custom_fields_in_quotation
erpnext.patches.v13_0.copy_custom_field_filters_to_website_item erpnext.patches.v13_0.copy_custom_field_filters_to_website_item
erpnext.patches.v14_0.discount_accounting_separation erpnext.patches.v14_0.discount_accounting_separation
erpnext.patches.v14_0.delete_employee_transfer_property_doctype

View File

@@ -0,0 +1,5 @@
import frappe
def execute():
frappe.delete_doc("DocType", "Employee Transfer Property", ignore_missing=True)

View File

@@ -12,6 +12,7 @@ import traceback
import frappe import frappe
import jwt import jwt
import requests
from frappe import _, bold from frappe import _, bold
from frappe.core.page.background_jobs.background_jobs import get_info from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.integrations.utils import make_get_request, make_post_request from frappe.integrations.utils import make_get_request, make_post_request
@@ -829,14 +830,25 @@ class GSPConnector:
return self.e_invoice_settings.auth_token return self.e_invoice_settings.auth_token
def make_request(self, request_type, url, headers=None, data=None): def make_request(self, request_type, url, headers=None, data=None):
if request_type == "post": try:
res = make_post_request(url, headers=headers, data=data) if request_type == "post":
else: res = make_post_request(url, headers=headers, data=data)
res = make_get_request(url, headers=headers, data=data) else:
res = make_get_request(url, headers=headers, data=data)
except requests.exceptions.HTTPError as e:
if e.response.status_code in [401, 403] and not hasattr(self, "token_auto_refreshed"):
self.auto_refresh_token()
headers = self.get_headers()
return self.make_request(request_type, url, headers, data)
self.log_request(url, headers, data, res) self.log_request(url, headers, data, res)
return res return res
def auto_refresh_token(self):
self.fetch_auth_token()
self.token_auto_refreshed = True
def log_request(self, url, headers, data, res): def log_request(self, url, headers, data, res):
headers.update({"password": self.credentials.password}) headers.update({"password": self.credentials.password})
request_log = frappe.get_doc( request_log = frappe.get_doc(

View File

@@ -27,6 +27,7 @@ class Quotation(SellingController):
self.set_status() self.set_status()
self.validate_uom_is_integer("stock_uom", "qty") self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till() self.validate_valid_till()
self.validate_shopping_cart_items()
self.set_customer_name() self.set_customer_name()
if self.items: if self.items:
self.with_items = 1 self.with_items = 1
@@ -49,6 +50,26 @@ class Quotation(SellingController):
if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date): if self.valid_till and getdate(self.valid_till) < getdate(self.transaction_date):
frappe.throw(_("Valid till date cannot be before transaction date")) frappe.throw(_("Valid till date cannot be before transaction date"))
def validate_shopping_cart_items(self):
if self.order_type != "Shopping Cart":
return
for item in self.items:
has_web_item = frappe.db.exists("Website Item", {"item_code": item.item_code})
# If variant is unpublished but template is published: valid
template = frappe.get_cached_value("Item", item.item_code, "variant_of")
if template and not has_web_item:
has_web_item = frappe.db.exists("Website Item", {"item_code": template})
if not has_web_item:
frappe.throw(
_("Row #{0}: Item {1} must have a Website Item for Shopping Cart Quotations").format(
item.idx, frappe.bold(item.item_code)
),
title=_("Unpublished Item"),
)
def has_sales_order(self): def has_sales_order(self):
return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1}) return frappe.db.get_value("Sales Order Item", {"prevdoc_docname": self.name, "docstatus": 1})

View File

@@ -130,6 +130,15 @@ class TestQuotation(FrappeTestCase):
quotation.submit() quotation.submit()
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name) self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
def test_shopping_cart_without_website_item(self):
if frappe.db.exists("Website Item", {"item_code": "_Test Item Home Desktop 100"}):
frappe.get_last_doc("Website Item", {"item_code": "_Test Item Home Desktop 100"}).delete()
quotation = frappe.copy_doc(test_records[0])
quotation.order_type = "Shopping Cart"
quotation.valid_till = getdate()
self.assertRaises(frappe.ValidationError, quotation.validate)
def test_create_quotation_with_margin(self): def test_create_quotation_with_margin(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order from erpnext.selling.doctype.quotation.quotation import make_sales_order
from erpnext.selling.doctype.sales_order.sales_order import ( from erpnext.selling.doctype.sales_order.sales_order import (

View File

@@ -4,8 +4,8 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Case from frappe.query_builder import Case, Order
from frappe.query_builder.functions import Coalesce, Sum from frappe.query_builder.functions import Coalesce, CombineDatetime, Sum
from frappe.utils import flt from frappe.utils import flt
@@ -121,24 +121,23 @@ def update_qty(bin_name, args):
bin_details = get_bin_details(bin_name) bin_details = get_bin_details(bin_name)
# actual qty is already updated by processing current voucher # actual qty is already updated by processing current voucher
actual_qty = bin_details.actual_qty actual_qty = bin_details.actual_qty or 0.0
sle = frappe.qb.DocType("Stock Ledger Entry")
# actual qty is not up to date in case of backdated transaction # actual qty is not up to date in case of backdated transaction
if future_sle_exists(args): if future_sle_exists(args):
actual_qty = ( last_sle_qty = (
frappe.db.get_value( frappe.qb.from_(sle)
"Stock Ledger Entry", .select(sle.qty_after_transaction)
filters={ .where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse")))
"item_code": args.get("item_code"), .orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
"warehouse": args.get("warehouse"), .orderby(sle.creation, order=Order.desc)
"is_cancelled": 0, .run()
},
fieldname="qty_after_transaction",
order_by="posting_date desc, posting_time desc, creation desc",
)
or 0.0
) )
if last_sle_qty:
actual_qty = last_sle_qty[0][0]
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty")) ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty")) reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty"))
indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty")) indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty"))

View File

@@ -23,6 +23,7 @@
"error_section", "error_section",
"error_log", "error_log",
"items_to_be_repost", "items_to_be_repost",
"affected_transactions",
"distinct_item_and_warehouse", "distinct_item_and_warehouse",
"current_index" "current_index"
], ],
@@ -172,12 +173,20 @@
"no_copy": 1, "no_copy": 1,
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
},
{
"fieldname": "affected_transactions",
"fieldtype": "Code",
"hidden": 1,
"label": "Affected Transactions",
"no_copy": 1,
"read_only": 1
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-03-30 07:22:48.520266", "modified": "2022-04-18 14:08:08.821602",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Repost Item Valuation", "name": "Repost Item Valuation",

View File

@@ -6,11 +6,14 @@ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
import erpnext import erpnext
from erpnext.accounts.utils import update_gl_entries_after from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle from erpnext.stock.stock_ledger import (
get_affected_transactions,
get_items_to_be_repost,
repost_future_sle,
)
class RepostItemValuation(Document): class RepostItemValuation(Document):
@@ -129,12 +132,12 @@ def repost(doc):
doc.set_status("Completed") doc.set_status("Completed")
except (Exception, JobTimeoutException): except Exception:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(traceback) frappe.log_error(traceback)
message = frappe.message_log.pop() message = frappe.message_log.pop() if frappe.message_log else ""
if traceback: if traceback:
message += "<br>" + "Traceback: <br>" + traceback message += "<br>" + "Traceback: <br>" + traceback
frappe.db.set_value(doc.doctype, doc.name, "error_log", message) frappe.db.set_value(doc.doctype, doc.name, "error_log", message)
@@ -170,6 +173,7 @@ def repost_sl_entries(doc):
], ],
allow_negative_stock=doc.allow_negative_stock, allow_negative_stock=doc.allow_negative_stock,
via_landed_cost_voucher=doc.via_landed_cost_voucher, via_landed_cost_voucher=doc.via_landed_cost_voucher,
doc=doc,
) )
@@ -177,27 +181,46 @@ def repost_gl_entries(doc):
if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)): if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
return return
# directly modified transactions
directly_dependent_transactions = _get_directly_dependent_vouchers(doc)
repost_affected_transaction = get_affected_transactions(doc)
repost_gle_for_stock_vouchers(
directly_dependent_transactions + list(repost_affected_transaction),
doc.posting_date,
doc.company,
)
def _get_directly_dependent_vouchers(doc):
"""Get stock vouchers that are directly affected by reposting
i.e. any one item-warehouse is present in the stock transaction"""
items = set()
warehouses = set()
if doc.based_on == "Transaction": if doc.based_on == "Transaction":
ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no) ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
doc_items, doc_warehouses = ref_doc.get_items_and_warehouses() doc_items, doc_warehouses = ref_doc.get_items_and_warehouses()
items.update(doc_items)
warehouses.update(doc_warehouses)
sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no) sles = get_items_to_be_repost(doc.voucher_type, doc.voucher_no)
sle_items = [sle.item_code for sle in sles] sle_items = {sle.item_code for sle in sles}
sle_warehouse = [sle.warehouse for sle in sles] sle_warehouses = {sle.warehouse for sle in sles}
items.update(sle_items)
items = list(set(doc_items).union(set(sle_items))) warehouses.update(sle_warehouses)
warehouses = list(set(doc_warehouses).union(set(sle_warehouse)))
else: else:
items = [doc.item_code] items.add(doc.item_code)
warehouses = [doc.warehouse] warehouses.add(doc.warehouse)
update_gl_entries_after( affected_vouchers = get_future_stock_vouchers(
doc.posting_date, posting_date=doc.posting_date,
doc.posting_time, posting_time=doc.posting_time,
for_warehouses=warehouses, for_warehouses=list(warehouses),
for_items=items, for_items=list(items),
company=doc.company, company=doc.company,
) )
return affected_vouchers
def notify_error_to_stock_managers(doc, traceback): def notify_error_to_stock_managers(doc, traceback):

View File

@@ -186,3 +186,10 @@ class TestRepostItemValuation(FrappeTestCase):
riv.db_set("status", "Skipped") riv.db_set("status", "Skipped")
riv.reload() riv.reload()
riv.cancel() # it should cancel now riv.cancel() # it should cancel now
def test_queue_progress_serialization(self):
# Make sure set/tuple -> list behaviour is retained.
self.assertEqual(
[["a", "b"], ["c", "d"]],
sorted(frappe.parse_json(frappe.as_json(set([("a", "b"), ("c", "d")])))),
)

View File

@@ -777,11 +777,11 @@ def auto_fetch_serial_number(
exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos))) exclude_sr_nos = get_serial_nos(clean_serial_no_string("\n".join(exclude_sr_nos)))
if batch_nos: if batch_nos:
batch_nos = safe_json_loads(batch_nos) batch_nos_list = safe_json_loads(batch_nos)
if isinstance(batch_nos, list): if isinstance(batch_nos_list, list):
filters.batch_no = batch_nos filters.batch_no = batch_nos_list
else: else:
filters.batch_no = [str(batch_nos)] filters.batch_no = [batch_nos]
if posting_date: if posting_date:
filters.expiry_date = posting_date filters.expiry_date = posting_date

View File

@@ -2,14 +2,15 @@
# See license.txt # See license.txt
import json import json
from operator import itemgetter
from uuid import uuid4 from uuid import uuid4
import frappe import frappe
from frappe.core.page.permission_manager.permission_manager import reset from frappe.core.page.permission_manager.permission_manager import reset
from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.query_builder.functions import CombineDatetime
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, today from frappe.utils import add_days, today
from frappe.utils.data import add_to_date
from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs from erpnext.accounts.doctype.gl_entry.gl_entry import rename_gle_sle_docs
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -1067,6 +1068,121 @@ class TestStockLedgerEntry(FrappeTestCase):
receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15) receipt2 = make_stock_entry(item_code=item, target=warehouse, qty=15, rate=15)
self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}]) self.assertSLEs(receipt2, [{"stock_queue": [[5, 15]], "stock_value_difference": 175}])
def test_dependent_gl_entry_reposting(self):
def _get_stock_credit(doc):
return frappe.db.get_value(
"GL Entry",
{
"voucher_no": doc.name,
"voucher_type": doc.doctype,
"is_cancelled": 0,
"account": "Stock In Hand - TCP1",
},
"sum(credit)",
)
def _day(days):
return add_to_date(date=today(), days=days)
item = make_item().name
A = "Stores - TCP1"
B = "Work In Progress - TCP1"
C = "Finished Goods - TCP1"
make_stock_entry(item_code=item, to_warehouse=A, qty=5, rate=10, posting_date=_day(0))
make_stock_entry(item_code=item, from_warehouse=A, to_warehouse=B, qty=5, posting_date=_day(1))
depdendent_consumption = make_stock_entry(
item_code=item, from_warehouse=B, qty=5, posting_date=_day(2)
)
self.assertEqual(50, _get_stock_credit(depdendent_consumption))
# backdated receipt - should trigger GL repost of all previous stock entries
bd_receipt = make_stock_entry(
item_code=item, to_warehouse=A, qty=5, rate=20, posting_date=_day(-1)
)
self.assertEqual(100, _get_stock_credit(depdendent_consumption))
# cancelling receipt should reset it back
bd_receipt.cancel()
self.assertEqual(50, _get_stock_credit(depdendent_consumption))
bd_receipt2 = make_stock_entry(
item_code=item, to_warehouse=A, qty=2, rate=20, posting_date=_day(-2)
)
# total as per FIFO -> 2 * 20 + 3 * 10 = 70
self.assertEqual(70, _get_stock_credit(depdendent_consumption))
# transfer WIP material to final destination and consume it all
depdendent_consumption.cancel()
make_stock_entry(item_code=item, from_warehouse=B, to_warehouse=C, qty=5, posting_date=_day(3))
final_consumption = make_stock_entry(
item_code=item, from_warehouse=C, qty=5, posting_date=_day(4)
)
# exact amount gets consumed
self.assertEqual(70, _get_stock_credit(final_consumption))
# cancel original backdated receipt - should repost A -> B -> C
bd_receipt2.cancel()
# original amount
self.assertEqual(50, _get_stock_credit(final_consumption))
def test_tie_breaking(self):
frappe.flags.dont_execute_stock_reposts = True
self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts")
item = make_item().name
warehouse = "_Test Warehouse - _TC"
posting_date = "2022-01-01"
posting_time = "00:00:01"
sle = frappe.qb.DocType("Stock Ledger Entry")
def ordered_qty_after_transaction():
return (
frappe.qb.from_(sle)
.select("qty_after_transaction")
.where((sle.item_code == item) & (sle.warehouse == warehouse) & (sle.is_cancelled == 0))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time))
.orderby(sle.creation)
).run(pluck=True)
first = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=10,
posting_time=posting_time,
posting_date=posting_date,
do_not_submit=True,
)
second = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=1,
posting_date=posting_date,
posting_time=posting_time,
do_not_submit=True,
)
first.submit()
second.submit()
self.assertEqual([10, 11], ordered_qty_after_transaction())
first.cancel()
self.assertEqual([1], ordered_qty_after_transaction())
backdated = make_stock_entry(
item_code=item,
to_warehouse=warehouse,
qty=1,
posting_date="2021-01-01",
posting_time=posting_time,
)
self.assertEqual([1, 2], ordered_qty_after_transaction())
backdated.cancel()
self.assertEqual([1], ordered_qty_after_transaction())
def create_repack_entry(**args): def create_repack_entry(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
from erpnext.accounts.utils import get_stock_and_account_balance from erpnext.accounts.utils import get_stock_and_account_balance
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
@@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase):
def tearDown(self): def tearDown(self):
frappe.flags.dont_execute_stock_reposts = None frappe.flags.dont_execute_stock_reposts = None
frappe.local.future_sle = {}
def test_reco_for_fifo(self): def test_reco_for_fifo(self):
self._test_reco_sle_gle("FIFO") self._test_reco_sle_gle("FIFO")
@@ -311,9 +312,8 @@ class TestStockReconciliation(FrappeTestCase):
SR4 | Reco | 0 | 6 (posting date: today-1) [backdated] SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
PR3 | PR | 1 | 7 (posting date: today) # can't post future PR PR3 | PR | 1 | 7 (posting date: today) # can't post future PR
""" """
item_code = "Backdated-Reco-Item" item_code = make_item().name
warehouse = "_Test Warehouse - _TC" warehouse = "_Test Warehouse - _TC"
create_item(item_code)
pr1 = make_purchase_receipt( pr1 = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3)
@@ -395,9 +395,8 @@ class TestStockReconciliation(FrappeTestCase):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.stock_ledger import NegativeStockError from erpnext.stock.stock_ledger import NegativeStockError
item_code = "Backdated-Reco-Item" item_code = make_item().name
warehouse = "_Test Warehouse - _TC" warehouse = "_Test Warehouse - _TC"
create_item(item_code)
pr1 = make_purchase_receipt( pr1 = make_purchase_receipt(
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2) item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -2)
@@ -444,9 +443,8 @@ class TestStockReconciliation(FrappeTestCase):
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.stock_ledger import NegativeStockError from erpnext.stock.stock_ledger import NegativeStockError
item_code = "Backdated-Reco-Cancellation-Item" item_code = make_item().name
warehouse = "_Test Warehouse - _TC" warehouse = "_Test Warehouse - _TC"
create_item(item_code)
sr = create_stock_reconciliation( sr = create_stock_reconciliation(
item_code=item_code, item_code=item_code,
@@ -487,9 +485,8 @@ class TestStockReconciliation(FrappeTestCase):
frappe.flags.dont_execute_stock_reposts = True frappe.flags.dont_execute_stock_reposts = True
frappe.db.rollback() frappe.db.rollback()
item_code = "Backdated-Reco-Cancellation-Item" item_code = make_item().name
warehouse = "_Test Warehouse - _TC" warehouse = "_Test Warehouse - _TC"
create_item(item_code)
sr = create_stock_reconciliation( sr = create_stock_reconciliation(
item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10) item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), 10)

View File

@@ -3,7 +3,7 @@
import copy import copy
import json import json
from typing import Optional from typing import Optional, Set, Tuple
import frappe import frappe
from frappe import _ from frappe import _
@@ -214,6 +214,7 @@ def repost_future_sle(
args = get_items_to_be_repost(voucher_type, voucher_no, doc) args = get_items_to_be_repost(voucher_type, voucher_no, doc)
distinct_item_warehouses = get_distinct_item_warehouse(args, doc) distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
affected_transactions = get_affected_transactions(doc)
i = get_current_index(doc) or 0 i = get_current_index(doc) or 0
while i < len(args): while i < len(args):
@@ -231,6 +232,7 @@ def repost_future_sle(
allow_negative_stock=allow_negative_stock, allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher, via_landed_cost_voucher=via_landed_cost_voucher,
) )
affected_transactions.update(obj.affected_transactions)
distinct_item_warehouses[ distinct_item_warehouses[
(args[i].get("item_code"), args[i].get("warehouse")) (args[i].get("item_code"), args[i].get("warehouse"))
@@ -250,10 +252,14 @@ def repost_future_sle(
i += 1 i += 1
if doc and i % 2 == 0: if doc and i % 2 == 0:
update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) update_args_in_repost_item_valuation(
doc, i, args, distinct_item_warehouses, affected_transactions
)
if doc and args: if doc and args:
update_args_in_repost_item_valuation(doc, i, args, distinct_item_warehouses) update_args_in_repost_item_valuation(
doc, i, args, distinct_item_warehouses, affected_transactions
)
def validate_item_warehouse(args): def validate_item_warehouse(args):
@@ -263,20 +269,22 @@ def validate_item_warehouse(args):
frappe.throw(_(validation_msg)) frappe.throw(_(validation_msg))
def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses): def update_args_in_repost_item_valuation(
frappe.db.set_value( doc, index, args, distinct_item_warehouses, affected_transactions
doc.doctype, ):
doc.name, doc.db_set(
{ {
"items_to_be_repost": json.dumps(args, default=str), "items_to_be_repost": json.dumps(args, default=str),
"distinct_item_and_warehouse": json.dumps( "distinct_item_and_warehouse": json.dumps(
{str(k): v for k, v in distinct_item_warehouses.items()}, default=str {str(k): v for k, v in distinct_item_warehouses.items()}, default=str
), ),
"current_index": index, "current_index": index,
}, "affected_transactions": frappe.as_json(affected_transactions),
}
) )
frappe.db.commit() if not frappe.flags.in_test:
frappe.db.commit()
frappe.publish_realtime( frappe.publish_realtime(
"item_reposting_progress", "item_reposting_progress",
@@ -313,6 +321,14 @@ def get_distinct_item_warehouse(args=None, doc=None):
return distinct_item_warehouses return distinct_item_warehouses
def get_affected_transactions(doc) -> Set[Tuple[str, str]]:
if not doc.affected_transactions:
return set()
transactions = frappe.parse_json(doc.affected_transactions)
return {tuple(transaction) for transaction in transactions}
def get_current_index(doc=None): def get_current_index(doc=None):
if doc and doc.current_index: if doc and doc.current_index:
return doc.current_index return doc.current_index
@@ -360,6 +376,7 @@ class update_entries_after(object):
self.new_items_found = False self.new_items_found = False
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict()) self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
self.affected_transactions: Set[Tuple[str, str]] = set()
self.data = frappe._dict() self.data = frappe._dict()
self.initialize_previous_data(self.args) self.initialize_previous_data(self.args)
@@ -518,6 +535,7 @@ class update_entries_after(object):
# previous sle data for this warehouse # previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse] self.wh_data = self.data[sle.warehouse]
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock): if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
# validate negative stock for serialized items, fifo valuation # validate negative stock for serialized items, fifo valuation

View File

@@ -9224,7 +9224,7 @@ Customer/Lead Name,Name des Kunden / Lead,
Unmarked Days,Nicht markierte Tage, Unmarked Days,Nicht markierte Tage,
Jan,Jan., Jan,Jan.,
Feb,Feb., Feb,Feb.,
Mar,Beschädigen, Mar,Mrz.,
Apr,Apr., Apr,Apr.,
Aug,Aug., Aug,Aug.,
Sep,Sep., Sep,Sep.,
Can't render this file because it is too large.

File diff suppressed because it is too large Load Diff