Merge branch 'develop' into accpunting_dimension_validate_ignore_new

This commit is contained in:
Deepesh Garg 2022-04-24 20:55:50 +05:30 committed by GitHub
commit 618fa0e8ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1590 additions and 1454 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

@ -24,17 +24,16 @@ frappe.ui.form.on("E Commerce Settings", {
); );
} }
frappe.model.with_doctype("Item", () => { frappe.model.with_doctype("Website Item", () => {
const web_item_meta = frappe.get_meta('Website Item'); const web_item_meta = frappe.get_meta('Website Item');
const valid_fields = web_item_meta.fields.filter( const valid_fields = web_item_meta.fields.filter(df =>
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname })); ).map(df =>
({ label: df.label, value: df.fieldname })
frm.fields_dict.filter_fields.grid.update_docfield_property(
'fieldname', 'fieldtype', 'Select'
); );
frm.fields_dict.filter_fields.grid.update_docfield_property(
frm.get_field("filter_fields").grid.update_docfield_property(
'fieldname', 'options', valid_fields 'fieldname', 'options', valid_fields
); );
}); });

View File

@ -27,7 +27,7 @@ class ECommerceSettings(Document):
self.is_redisearch_loaded = is_search_module_loaded() self.is_redisearch_loaded = is_search_module_loaded()
def validate(self): def validate(self):
self.validate_field_filters() self.validate_field_filters(self.filter_fields, self.enable_field_filters)
self.validate_attribute_filters() self.validate_attribute_filters()
self.validate_checkout() self.validate_checkout()
self.validate_search_index_fields() self.validate_search_index_fields()
@ -51,21 +51,22 @@ class ECommerceSettings(Document):
define_autocomplete_dictionary() define_autocomplete_dictionary()
create_website_items_index() create_website_items_index()
def validate_field_filters(self): @staticmethod
if not (self.enable_field_filters and self.filter_fields): def validate_field_filters(filter_fields, enable_field_filters):
if not (enable_field_filters and filter_fields):
return return
item_meta = frappe.get_meta("Item") web_item_meta = frappe.get_meta("Website Item")
valid_fields = [ valid_fields = [
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"] df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
] ]
for f in self.filter_fields: for row in filter_fields:
if f.fieldname not in valid_fields: if row.fieldname not in valid_fields:
frappe.throw( frappe.throw(
_( _(
"Filter Fields Row #{0}: Fieldname <b>{1}</b> must be of type 'Link' or 'Table MultiSelect'" "Filter Fields Row #{0}: Fieldname {1} must be of type 'Link' or 'Table MultiSelect'"
).format(f.idx, f.fieldname) ).format(row.idx, frappe.bold(row.fieldname))
) )
def validate_attribute_filters(self): def validate_attribute_filters(self):

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*- # Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest import unittest
@ -11,44 +10,35 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
class TestECommerceSettings(unittest.TestCase): class TestECommerceSettings(unittest.TestCase):
def setUp(self): def tearDown(self):
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """) frappe.db.rollback()
def get_cart_settings(self):
return frappe.get_doc({"doctype": "E Commerce Settings", "company": "_Test Company"})
# NOTE: Exchangrate API has all enabled currencies that ERPNext supports.
# We aren't checking just currency exchange record anymore
# while validating price list currency exchange rate to that of company.
# The API is being used to fetch the rate which again almost always
# gives back a valid value (for valid currencies).
# This makes the test obsolete.
# Commenting because im not sure if there's a better test we can write
# def test_exchange_rate_exists(self):
# frappe.db.sql("""delete from `tabCurrency Exchange`""")
# cart_settings = self.get_cart_settings()
# cart_settings.price_list = "_Test Price List Rest of the World"
# self.assertRaises(ShoppingCartSetupError, cart_settings.validate_exchange_rates_exist)
# from erpnext.setup.doctype.currency_exchange.test_currency_exchange import (
# test_records as currency_exchange_records,
# )
# frappe.get_doc(currency_exchange_records[0]).insert()
# cart_settings.validate_exchange_rates_exist()
def test_tax_rule_validation(self): def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0") frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
frappe.db.commit() # nosemgrep frappe.db.commit() # nosemgrep
cart_settings = self.get_cart_settings() cart_settings = frappe.get_doc("E Commerce Settings")
cart_settings.enabled = 1 cart_settings.enabled = 1
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"): if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule) self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1") frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 1")
def test_invalid_filter_fields(self):
"Check if Item fields are blocked in E Commerce Settings filter fields."
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
setup_e_commerce_settings({"enable_field_filters": 1})
create_custom_field(
"Item",
dict(owner="Administrator", fieldname="test_data", label="Test", fieldtype="Data"),
)
settings = frappe.get_doc("E Commerce Settings")
settings.append("filter_fields", {"fieldname": "test_data"})
self.assertRaises(frappe.ValidationError, settings.save)
def setup_e_commerce_settings(values_dict): def setup_e_commerce_settings(values_dict):
"Accepts a dict of values that updates E Commerce Settings." "Accepts a dict of values that updates E Commerce Settings."

View File

@ -22,12 +22,14 @@ class ProductFiltersBuilder:
fields, filter_data = [], [] fields, filter_data = [], []
filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings filter_fields = [row.fieldname for row in self.doc.filter_fields] # fields in settings
# filter valid field filters i.e. those that exist in Item # filter valid field filters i.e. those that exist in Website Item
item_meta = frappe.get_meta("Item", cached=True) web_item_meta = frappe.get_meta("Website Item", cached=True)
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] fields = [
web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)
]
for df in fields: for df in fields:
item_filters, item_or_filters = {"published_in_website": 1}, [] item_filters, item_or_filters = {"published": 1}, []
link_doctype_values = self.get_filtered_link_doctype_records(df) link_doctype_values = self.get_filtered_link_doctype_records(df)
if df.fieldtype == "Link": if df.fieldtype == "Link":
@ -50,9 +52,13 @@ class ProductFiltersBuilder:
] ]
) )
# exclude variants if mentioned in settings
if frappe.db.get_single_value("E Commerce Settings", "hide_variants"):
item_filters["variant_of"] = ["is", "not set"]
# Get link field values attached to published items # Get link field values attached to published items
item_values = frappe.get_all( item_values = frappe.get_all(
"Item", "Website Item",
fields=[df.fieldname], fields=[df.fieldname],
filters=item_filters, filters=item_filters,
or_filters=item_or_filters, or_filters=item_or_filters,

View File

@ -277,6 +277,54 @@ class TestProductDataEngine(unittest.TestCase):
# tear down # tear down
setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0}) setup_e_commerce_settings({"enable_attribute_filters": 1, "hide_variants": 0})
def test_custom_field_as_filter(self):
"Test if custom field functions as filter correctly."
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
create_custom_field(
"Website Item",
dict(
owner="Administrator",
fieldname="supplier",
label="Supplier",
fieldtype="Link",
options="Supplier",
insert_after="on_backorder",
),
)
frappe.db.set_value(
"Website Item", {"item_code": "Test 11I Laptop"}, "supplier", "_Test Supplier"
)
frappe.db.set_value(
"Website Item", {"item_code": "Test 12I Laptop"}, "supplier", "_Test Supplier 1"
)
settings = frappe.get_doc("E Commerce Settings")
settings.append("filter_fields", {"fieldname": "supplier"})
settings.save()
filter_engine = ProductFiltersBuilder()
field_filters = filter_engine.get_field_filters()
custom_filter = field_filters[1]
filter_values = custom_filter[1]
self.assertEqual(custom_filter[0].options, "Supplier")
self.assertEqual(len(filter_values), 2)
self.assertIn("_Test Supplier", filter_values)
# test if custom filter works in query
field_filters = {"supplier": "_Test Supplier 1"}
engine = ProductQuery()
result = engine.query(
attributes={}, fields=field_filters, search_term=None, start=0, item_group=None
)
items = result.get("items")
# check if only 'Raw Material' are fetched in the right order
self.assertEqual(len(items), 1)
self.assertEqual(items[0].get("item_code"), "Test 12I Laptop")
def create_variant_web_item(): def create_variant_web_item():
"Create Variant and Template Website Items." "Create Variant and Template Website Items."

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

@ -364,3 +364,5 @@ erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022 erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
erpnext.patches.v13_0.update_expense_claim_status_for_paid_advances 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.v14_0.delete_employee_transfer_property_doctype

View File

@ -0,0 +1,94 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def execute():
"Add Field Filters, that are not standard fields in Website Item, as Custom Fields."
def move_table_multiselect_data(docfield):
"Copy child table data (Table Multiselect) from Item to Website Item for a docfield."
table_multiselect_data = get_table_multiselect_data(docfield)
field = docfield.fieldname
for row in table_multiselect_data:
# add copied multiselect data rows in Website Item
web_item = frappe.db.get_value("Website Item", {"item_code": row.parent})
web_item_doc = frappe.get_doc("Website Item", web_item)
child_doc = frappe.new_doc(docfield.options, web_item_doc, field)
for field in ["name", "creation", "modified", "idx"]:
row[field] = None
child_doc.update(row)
child_doc.parenttype = "Website Item"
child_doc.parent = web_item
child_doc.insert()
def get_table_multiselect_data(docfield):
child_table = frappe.qb.DocType(docfield.options)
item = frappe.qb.DocType("Item")
table_multiselect_data = ( # query table data for field
frappe.qb.from_(child_table)
.join(item)
.on(item.item_code == child_table.parent)
.select(child_table.star)
.where((child_table.parentfield == docfield.fieldname) & (item.published_in_website == 1))
).run(as_dict=True)
return table_multiselect_data
settings = frappe.get_doc("E Commerce Settings")
if not (settings.enable_field_filters or settings.filter_fields):
return
item_meta = frappe.get_meta("Item")
valid_item_fields = [
df.fieldname for df in item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
]
web_item_meta = frappe.get_meta("Website Item")
valid_web_item_fields = [
df.fieldname for df in web_item_meta.fields if df.fieldtype in ["Link", "Table MultiSelect"]
]
for row in settings.filter_fields:
# skip if illegal field
if row.fieldname not in valid_item_fields:
continue
# if Item field is not in Website Item, add it as a custom field
if row.fieldname not in valid_web_item_fields:
df = item_meta.get_field(row.fieldname)
create_custom_field(
"Website Item",
dict(
owner="Administrator",
fieldname=df.fieldname,
label=df.label,
fieldtype=df.fieldtype,
options=df.options,
description=df.description,
read_only=df.read_only,
no_copy=df.no_copy,
insert_after="on_backorder",
),
)
# map field values
if df.fieldtype == "Table MultiSelect":
move_table_multiselect_data(df)
else:
frappe.db.sql( # nosemgrep
"""
UPDATE `tabWebsite Item` wi, `tabItem` i
SET wi.{0} = i.{0}
WHERE wi.item_code = i.item_code
""".format(
row.fieldname
)
)

View File

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

View File

@ -1,76 +1,31 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0, "creation": "2018-12-31 17:06:08.716134",
"allow_guest_to_view": 0, "doctype": "DocType",
"allow_import": 0, "editable_grid": 1,
"allow_rename": 0, "engine": "InnoDB",
"beta": 0, "field_order": [
"creation": "2018-12-31 17:06:08.716134", "fieldname"
"custom": 0, ],
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "fieldname",
"allow_in_quick_entry": 0, "fieldtype": "Autocomplete",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Fieldname"
"collapsible": 0,
"columns": 0,
"fieldname": "fieldname",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Fieldname",
"length": 0,
"no_copy": 0,
"options": "",
"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, "istable": 1,
"hide_heading": 0, "links": [],
"hide_toolbar": 0, "modified": "2022-04-18 18:55:17.835666",
"idx": 0, "modified_by": "Administrator",
"image_view": 0, "module": "Portal",
"in_create": 0, "name": "Website Filter Field",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0, "permissions": [],
"istable": 1, "quick_entry": 1,
"max_attachments": 0, "sort_field": "modified",
"modified": "2019-01-01 18:26:11.550380", "sort_order": "DESC",
"modified_by": "Administrator", "states": [],
"module": "Portal", "track_changes": 1
"name": "Website Filter Field",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"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,
"track_views": 0
} }

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

@ -72,17 +72,16 @@ frappe.ui.form.on("Item Group", {
}); });
} }
frappe.model.with_doctype('Item', () => { frappe.model.with_doctype('Website Item', () => {
const item_meta = frappe.get_meta('Item'); const web_item_meta = frappe.get_meta('Website Item');
const valid_fields = item_meta.fields.filter( const valid_fields = web_item_meta.fields.filter(df =>
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname })); ).map(df =>
({ label: df.label, value: df.fieldname })
frm.fields_dict.filter_fields.grid.update_docfield_property(
'fieldname', 'fieldtype', 'Select'
); );
frm.fields_dict.filter_fields.grid.update_docfield_property(
frm.get_field("filter_fields").grid.update_docfield_property(
'fieldname', 'options', valid_fields 'fieldname', 'options', valid_fields
); );
}); });

View File

@ -11,6 +11,7 @@ from frappe.utils.nestedset import NestedSet
from frappe.website.utils import clear_cache from frappe.website.utils import clear_cache
from frappe.website.website_generator import WebsiteGenerator from frappe.website.website_generator import WebsiteGenerator
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import ECommerceSettings
from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder from erpnext.e_commerce.product_data_engine.filters import ProductFiltersBuilder
@ -35,6 +36,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
self.make_route() self.make_route()
self.validate_item_group_defaults() self.validate_item_group_defaults()
ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True)
def on_update(self): def on_update(self):
NestedSet.on_update(self) NestedSet.on_update(self)

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,7 +2,7 @@
# See license.txt # See license.txt
import json import json
from operator import itemgetter from datetime import timedelta
from uuid import uuid4 from uuid import uuid4
import frappe import frappe
@ -10,6 +10,7 @@ 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.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,64 @@ 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 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