Merge branch 'develop' of https://github.com/frappe/erpnext into round_off_account_cost_center

This commit is contained in:
Deepesh Garg 2022-04-23 21:40:38 +05:30
commit 783793deac
41 changed files with 1487 additions and 1461 deletions

View File

@ -12,6 +12,7 @@ jobs:
uses: actions/checkout@v2
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js v14
uses: actions/setup-node@v2
with:
@ -21,5 +22,10 @@ jobs:
npm install @semantic-release/git @semantic-release/exec --no-save
- name: Create Release
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
GIT_AUTHOR_NAME: "Frappe PR Bot"
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release

View File

@ -1,10 +1,17 @@
import unittest
import frappe
from frappe.test_runner import make_test_objects
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.stock_entry.stock_entry_utils import make_stock_entry
class TestUtils(unittest.TestCase):
@ -47,6 +54,25 @@ class TestUtils(unittest.TestCase):
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 = [
{

View File

@ -3,6 +3,7 @@
from json import loads
from typing import List, Tuple
import frappe
import frappe.defaults
@ -1122,6 +1123,9 @@ def update_gl_entries_after(
def repost_gle_for_stock_vouchers(
stock_vouchers, posting_date, company=None, warehouse_account=None
):
if not stock_vouchers:
return
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql(
"""delete from `tabGL Entry`
@ -1129,6 +1133,8 @@ def repost_gle_for_stock_vouchers(
(voucher_type, voucher_no),
)
stock_vouchers = sort_stock_vouchers_by_posting_date(stock_vouchers)
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
@ -1149,6 +1155,27 @@ def repost_gle_for_stock_vouchers(
_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(
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 valid_fields = web_item_meta.fields.filter(
df => ["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname }));
frm.fields_dict.filter_fields.grid.update_docfield_property(
'fieldname', 'fieldtype', 'Select'
const valid_fields = web_item_meta.fields.filter(df =>
["Link", "Table MultiSelect"].includes(df.fieldtype) && !df.hidden
).map(df =>
({ label: df.label, value: df.fieldname })
);
frm.fields_dict.filter_fields.grid.update_docfield_property(
frm.get_field("filter_fields").grid.update_docfield_property(
'fieldname', 'options', valid_fields
);
});

View File

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

View File

@ -1,5 +1,4 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
@ -11,44 +10,35 @@ from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
class TestECommerceSettings(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabSingles` where doctype="Shipping Cart Settings" """)
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 tearDown(self):
frappe.db.rollback()
def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
frappe.db.commit() # nosemgrep
cart_settings = self.get_cart_settings()
cart_settings = frappe.get_doc("E Commerce Settings")
cart_settings.enabled = 1
if not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1}, "name"):
self.assertRaises(ShoppingCartSetupError, cart_settings.validate_tax_rule)
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):
"Accepts a dict of values that updates E Commerce Settings."

View File

@ -22,12 +22,14 @@ class ProductFiltersBuilder:
fields, filter_data = [], []
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
item_meta = frappe.get_meta("Item", cached=True)
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
# filter valid field filters i.e. those that exist in Website Item
web_item_meta = frappe.get_meta("Website Item", cached=True)
fields = [
web_item_meta.get_field(field) for field in filter_fields if web_item_meta.has_field(field)
]
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)
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
item_values = frappe.get_all(
"Item",
"Website Item",
fields=[df.fieldname],
filters=item_filters,
or_filters=item_or_filters,

View File

@ -277,6 +277,54 @@ class TestProductDataEngine(unittest.TestCase):
# tear down
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():
"Create Variant and Template Website Items."

View File

@ -62,6 +62,8 @@
"holiday_list",
"default_shift",
"salary_information",
"salary_currency",
"ctc",
"salary_mode",
"payroll_cost_center",
"column_break_52",
@ -807,13 +809,25 @@
"fieldtype": "Link",
"label": "Shift Request Approver",
"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",
"idx": 24,
"image_field": "image",
"links": [],
"modified": "2022-03-22 13:44:37.088519",
"modified": "2022-04-22 16:21:55.811983",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",

View File

@ -1,397 +1,172 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "HR-EMP-PRO-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 18:33:59.476562",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"autoname": "HR-EMP-PRO-.YYYY.-.#####",
"creation": "2018-04-13 18:33:59.476562",
"doctype": "DocType",
"editable_grid": 1,
"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": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"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
},
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"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
},
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"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
},
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"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
},
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"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
},
"fieldname": "promotion_date",
"fieldtype": "Date",
"label": "Promotion Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"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
},
"fetch_from": "employee.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"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
},
"description": "Set the properties that should be updated in the Employee master on promotion submission",
"fieldname": "details_section",
"fieldtype": "Section Break",
"label": "Employee Promotion Details"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"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
},
"fieldname": "promotion_details",
"fieldtype": "Table",
"options": "Employee Property History",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"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",
"length": 0,
"no_copy": 1,
"options": "Employee Promotion",
"permlevel": 0,
"print_hide": 1,
"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
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Employee Promotion",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "salary_details_section",
"fieldtype": "Section Break",
"label": "Salary Details"
},
{
"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,
"issingle": 0,
"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",
],
"is_submittable": 1,
"links": [],
"modified": "2022-04-22 18:47:10.168744",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Promotion",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 0
},
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Employee",
"share": 1
},
{
"amend": 0,
"cancel": 0,
"create": 1,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"create": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR User",
"share": 1,
"submit": 1,
"write": 1
},
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "HR Manager",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "employee_name",
"track_changes": 1
}

View File

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

View File

@ -4,21 +4,22 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_employee
class TestEmployeePromotion(unittest.TestCase):
class TestEmployeePromotion(FrappeTestCase):
def setUp(self):
self.employee = make_employee("employee@promotions.com")
frappe.db.sql("""delete from `tabEmployee Promotion`""")
frappe.db.delete("Employee Promotion")
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",
"employee": self.employee,
"employee": employee,
"promotion_details": [
{
"property": "Designation",
@ -29,10 +30,68 @@ class TestEmployeePromotion(unittest.TestCase):
],
}
)
promotion_obj.promotion_date = add_days(getdate(), 1)
promotion_obj.save()
self.assertRaises(frappe.DocstatusTransitionError, promotion_obj.submit)
promotion = frappe.get_doc("Employee Promotion", promotion_obj.name)
promotion.promotion_date = add_days(getdate(), 1)
self.assertRaises(frappe.DocstatusTransitionError, promotion.submit)
promotion.promotion_date = getdate()
promotion.submit()
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

@ -121,7 +121,7 @@ def has_overlapping_timings(shift_1: str, shift_2: str) -> bool:
@frappe.whitelist()
def get_events(start, end, filters=None):
events = []
from frappe.desk.calendar import get_event_conditions
employee = frappe.db.get_value(
"Employee", {"user_id": frappe.session.user}, ["name", "company"], as_dict=True
@ -132,20 +132,22 @@ def get_events(start, end, filters=None):
employee = ""
company = frappe.db.get_value("Global Defaults", None, "default_company")
from frappe.desk.reportview import get_filters_cond
conditions = get_filters_cond("Shift Assignment", filters, [])
add_assignments(events, start, end, conditions=conditions)
conditions = get_event_conditions("Shift Assignment", filters)
events = add_assignments(start, end, conditions=conditions)
return events
def add_assignments(events, start, end, conditions=None):
def add_assignments(start, end, conditions=None):
events = []
query = """select name, start_date, end_date, employee_name,
employee, docstatus, shift_type
from `tabShift Assignment` where
start_date >= %(start_date)s
or end_date <= %(end_date)s
or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
(
start_date >= %(start_date)s
or end_date <= %(end_date)s
or (%(start_date)s between start_date and end_date and %(end_date)s between start_date and end_date)
)
and docstatus = 1"""
if conditions:
query += conditions

View File

@ -8,7 +8,7 @@ from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, getdate, nowdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.shift_assignment.shift_assignment import OverlappingShiftError
from erpnext.hr.doctype.shift_assignment.shift_assignment import OverlappingShiftError, get_events
from erpnext.hr.doctype.shift_type.test_shift_type import make_shift_assignment, setup_shift_type
test_dependencies = ["Shift Type"]
@ -154,3 +154,18 @@ class TestShiftAssignment(FrappeTestCase):
shift_type = setup_shift_type(shift_type="Shift 2", start_time="13:00:00", end_time="15:00:00")
date = getdate()
make_shift_assignment(shift_type.name, employee, date)
def test_shift_assignment_calendar(self):
employee1 = make_employee("test_shift_assignment1@example.com", company="_Test Company")
employee2 = make_employee("test_shift_assignment2@example.com", company="_Test Company")
shift_type = setup_shift_type(shift_type="Shift 1", start_time="08:00:00", end_time="12:00:00")
date = getdate()
shift1 = make_shift_assignment(shift_type.name, employee1, date)
make_shift_assignment(shift_type.name, employee2, date)
events = get_events(
start=date, end=date, filters=[["Shift Assignment", "employee", "=", employee1, False]]
)
self.assertEqual(len(events), 1)
self.assertEqual(events[0]["name"], shift1.name)

View File

@ -8,39 +8,65 @@ frappe.ui.form.on(cur_frm.doctype, {
};
});
},
onload: function(frm){
if(frm.doc.__islocal){
if(frm.doctype == "Employee Promotion"){
frm.doc.promotion_details = [];
}else if (frm.doctype == "Employee Transfer") {
frm.doc.transfer_details = [];
}
}
onload: function(frm) {
if (frm.doc.__islocal)
frm.trigger("clear_property_table");
},
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) {
var table;
if(frm.doctype == "Employee Promotion"){
let table;
if (frm.doctype == "Employee Promotion") {
table = "promotion_details";
}else if (frm.doctype == "Employee Transfer") {
} else if (frm.doctype == "Employee Transfer") {
table = "transfer_details";
}
if(!table){return;}
cur_frm.fields_dict[table].grid.wrapper.find('.grid-add-row').hide();
cur_frm.fields_dict[table].grid.add_custom_button(__('Add Row'), () => {
if(!frm.doc.employee){
frappe.msgprint(__("Please select Employee"));
if (!table)
return;
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;
}
frappe.call({
method: 'erpnext.hr.utils.get_employee_fields_label',
callback: function(r) {
if(r.message){
show_dialog(frm, table, r.message);
const allowed_fields = [];
const exclude_fields = ["naming_series", "employee", "first_name", "middle_name", "last_name", "marital_status", "ctc",
"employee_name", "status", "image", "gender", "date_of_birth", "date_of_joining", "lft", "rgt", "old_parent"];
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({
title: "Update Property",
fields: [
{fieldname: "property", label: __('Select Property'), fieldtype:"Select", options: field_labels},
{fieldname: "current", fieldtype: "Data", label:__('Current'), read_only: true},
{fieldname: "field_html", fieldtype: "HTML"}
{fieldname: "property", label: __("Select Property"), fieldtype: "Autocomplete", options: field_labels},
{fieldname: "current", fieldtype: "Data", label: __("Current"), read_only: true},
{fieldname: "new_value", fieldtype: "Data", label: __("New")}
],
primary_action_label: __('Add to Details'),
primary_action_label: __("Add to Details"),
primary_action: () => {
d.get_primary_btn().attr('disabled', true);
if(d.data) {
var input = $('[data-fieldname="field_html"] input');
d.data.new = input.val();
$(input).remove();
d.get_primary_btn().attr("disabled", true);
if (d.data) {
d.data.new = d.get_values().new_value;
add_to_details(frm, d, table);
}
}
});
d.fields_dict["property"].df.onchange = () => {
let property = d.get_values().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',
args: {employee: frm.doc.employee, fieldname: property},
callback: function(r) {
if(r.message){
if (r.message) {
d.data.current = r.message.value;
d.data.property = r.message.label;
d.fields_dict.field_html.$wrapper.html("");
d.set_value('current', r.message.value);
render_dynamic_field(d, r.message.datatype, r.message.options, property);
d.get_primary_btn().attr('disabled', false);
@ -95,25 +120,26 @@ var render_dynamic_field = function(d, fieldtype, options, fieldname) {
df: {
"fieldtype": fieldtype,
"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
});
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) {
let data = d.data;
if(data.fieldname){
if(validate_duplicate(frm, table, data.fieldname)){
frappe.show_alert({message:__("Property already added"), indicator:'orange'});
if (data.fieldname) {
if (validate_duplicate(frm, table, data.fieldname)) {
frappe.show_alert({message: __("Property already added"), indicator: "orange"});
return false;
}
if(data.current == data.new){
frappe.show_alert({message:__("Nothing to change"), indicator:'orange'});
d.get_primary_btn().attr('disabled', false);
if (data.current == data.new) {
frappe.show_alert({message: __("Nothing to change"), indicator: "orange"});
d.get_primary_btn().attr("disabled", false);
return false;
}
frm.add_child(table, {
@ -123,13 +149,16 @@ var add_to_details = function(frm, d, table) {
new: data.new
});
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('current', "");
frappe.show_alert({message:__("Added to details"),indicator:'green'});
d.set_value("current", "");
frappe.show_alert({message: __("Added to details"), indicator: "green"});
d.data = {};
}else {
frappe.show_alert({message:__("Value missing"),indicator:'red'});
} else {
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.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()
def get_employee_field_property(employee, fieldname):
if employee and fieldname:

View File

@ -73,10 +73,22 @@ frappe.ui.form.on('Job Card', {
if (frm.doc.docstatus == 0 && !frm.is_new() &&
(frm.doc.for_quantity > frm.doc.total_completed_qty || !frm.doc.for_quantity)
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons");
// if Job Card is link to Work Order, the job card must not be able to start if Work Order not "Started"
// and if stock mvt for WIP is required
if (frm.doc.work_order) {
frappe.db.get_value('Work Order', frm.doc.work_order, ['skip_transfer', 'status'], (result) => {
if (result.skip_transfer === 1 || result.status == 'In Process') {
frm.trigger("prepare_timer_buttons");
}
});
} else {
frm.trigger("prepare_timer_buttons");
}
}
frm.trigger("setup_quality_inspection");
if (frm.doc.work_order) {
frappe.db.get_value('Work Order', frm.doc.work_order,
'transfer_material_against').then((r) => {

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

View File

@ -12,6 +12,7 @@ import traceback
import frappe
import jwt
import requests
from frappe import _, bold
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.integrations.utils import make_get_request, make_post_request
@ -829,14 +830,25 @@ class GSPConnector:
return self.e_invoice_settings.auth_token
def make_request(self, request_type, url, headers=None, data=None):
if request_type == "post":
res = make_post_request(url, headers=headers, data=data)
else:
res = make_get_request(url, headers=headers, data=data)
try:
if request_type == "post":
res = make_post_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)
return res
def auto_refresh_token(self):
self.fetch_auth_token()
self.token_auto_refreshed = True
def log_request(self, url, headers, data, res):
headers.update({"password": self.credentials.password})
request_log = frappe.get_doc(
@ -1074,7 +1086,7 @@ class GSPConnector:
"Distance": cint(eway_bill_details.distance),
"TransMode": eway_bill_details.mode_of_transport,
"TransId": eway_bill_details.gstin,
"TransName": eway_bill_details.transporter,
"TransName": eway_bill_details.name,
"TrnDocDt": eway_bill_details.document_date,
"TrnDocNo": eway_bill_details.document_name,
"VehNo": eway_bill_details.vehicle_no,

View File

@ -27,6 +27,7 @@ class Quotation(SellingController):
self.set_status()
self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till()
self.validate_shopping_cart_items()
self.set_customer_name()
if self.items:
self.with_items = 1
@ -49,6 +50,26 @@ class Quotation(SellingController):
if self.valid_till and getdate(self.valid_till) < getdate(self.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):
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()
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):
from erpnext.selling.doctype.quotation.quotation import make_sales_order
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', () => {
const item_meta = frappe.get_meta('Item');
frappe.model.with_doctype('Website Item', () => {
const web_item_meta = frappe.get_meta('Website Item');
const valid_fields = item_meta.fields.filter(
df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
).map(df => ({ label: df.label, value: df.fieldname }));
frm.fields_dict.filter_fields.grid.update_docfield_property(
'fieldname', 'fieldtype', 'Select'
const valid_fields = web_item_meta.fields.filter(df =>
['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
).map(df =>
({ label: df.label, value: df.fieldname })
);
frm.fields_dict.filter_fields.grid.update_docfield_property(
frm.get_field("filter_fields").grid.update_docfield_property(
'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.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
@ -35,6 +36,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
self.make_route()
self.validate_item_group_defaults()
ECommerceSettings.validate_field_filters(self.filter_fields, enable_field_filters=True)
def on_update(self):
NestedSet.on_update(self)

View File

@ -4,8 +4,8 @@
import frappe
from frappe.model.document import Document
from frappe.query_builder import Case
from frappe.query_builder.functions import Coalesce, Sum
from frappe.query_builder import Case, Order
from frappe.query_builder.functions import Coalesce, CombineDatetime, Sum
from frappe.utils import flt
@ -121,24 +121,23 @@ def update_qty(bin_name, args):
bin_details = get_bin_details(bin_name)
# 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
if future_sle_exists(args):
actual_qty = (
frappe.db.get_value(
"Stock Ledger Entry",
filters={
"item_code": args.get("item_code"),
"warehouse": args.get("warehouse"),
"is_cancelled": 0,
},
fieldname="qty_after_transaction",
order_by="posting_date desc, posting_time desc, creation desc",
)
or 0.0
last_sle_qty = (
frappe.qb.from_(sle)
.select(sle.qty_after_transaction)
.where((sle.item_code == args.get("item_code")) & (sle.warehouse == args.get("warehouse")))
.orderby(CombineDatetime(sle.posting_date, sle.posting_time), order=Order.desc)
.orderby(sle.creation, order=Order.desc)
.run()
)
if last_sle_qty:
actual_qty = last_sle_qty[0][0]
ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_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"))

View File

@ -23,6 +23,7 @@
"error_section",
"error_log",
"items_to_be_repost",
"affected_transactions",
"distinct_item_and_warehouse",
"current_index"
],
@ -172,12 +173,20 @@
"no_copy": 1,
"print_hide": 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,
"is_submittable": 1,
"links": [],
"modified": "2022-03-30 07:22:48.520266",
"modified": "2022-04-18 14:08:08.821602",
"modified_by": "Administrator",
"module": "Stock",
"name": "Repost Item Valuation",

View File

@ -6,11 +6,14 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, get_link_to_form, get_weekday, now, nowtime
from frappe.utils.user import get_users_with_role
from rq.timeouts import JobTimeoutException
import erpnext
from erpnext.accounts.utils import update_gl_entries_after
from erpnext.stock.stock_ledger import get_items_to_be_repost, repost_future_sle
from erpnext.accounts.utils import get_future_stock_vouchers, repost_gle_for_stock_vouchers
from erpnext.stock.stock_ledger import (
get_affected_transactions,
get_items_to_be_repost,
repost_future_sle,
)
class RepostItemValuation(Document):
@ -129,12 +132,12 @@ def repost(doc):
doc.set_status("Completed")
except (Exception, JobTimeoutException):
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(traceback)
message = frappe.message_log.pop()
message = frappe.message_log.pop() if frappe.message_log else ""
if traceback:
message += "<br>" + "Traceback: <br>" + traceback
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,
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)):
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":
ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
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)
sle_items = [sle.item_code for sle in sles]
sle_warehouse = [sle.warehouse for sle in sles]
items = list(set(doc_items).union(set(sle_items)))
warehouses = list(set(doc_warehouses).union(set(sle_warehouse)))
sle_items = {sle.item_code for sle in sles}
sle_warehouses = {sle.warehouse for sle in sles}
items.update(sle_items)
warehouses.update(sle_warehouses)
else:
items = [doc.item_code]
warehouses = [doc.warehouse]
items.add(doc.item_code)
warehouses.add(doc.warehouse)
update_gl_entries_after(
doc.posting_date,
doc.posting_time,
for_warehouses=warehouses,
for_items=items,
affected_vouchers = get_future_stock_vouchers(
posting_date=doc.posting_date,
posting_time=doc.posting_time,
for_warehouses=list(warehouses),
for_items=list(items),
company=doc.company,
)
return affected_vouchers
def notify_error_to_stock_managers(doc, traceback):

View File

@ -186,3 +186,10 @@ class TestRepostItemValuation(FrappeTestCase):
riv.db_set("status", "Skipped")
riv.reload()
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)))
if batch_nos:
batch_nos = safe_json_loads(batch_nos)
if isinstance(batch_nos, list):
filters.batch_no = batch_nos
batch_nos_list = safe_json_loads(batch_nos)
if isinstance(batch_nos_list, list):
filters.batch_no = batch_nos_list
else:
filters.batch_no = [str(batch_nos)]
filters.batch_no = [batch_nos]
if posting_date:
filters.expiry_date = posting_date

View File

@ -2,7 +2,7 @@
# See license.txt
import json
from operator import itemgetter
from datetime import timedelta
from uuid import uuid4
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.tests.utils import FrappeTestCase
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.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)
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):
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 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.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
@ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase):
def tearDown(self):
frappe.flags.dont_execute_stock_reposts = None
frappe.local.future_sle = {}
def test_reco_for_fifo(self):
self._test_reco_sle_gle("FIFO")
@ -311,9 +312,8 @@ class TestStockReconciliation(FrappeTestCase):
SR4 | Reco | 0 | 6 (posting date: today-1) [backdated]
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"
create_item(item_code)
pr1 = make_purchase_receipt(
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.stock_ledger import NegativeStockError
item_code = "Backdated-Reco-Item"
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
create_item(item_code)
pr1 = make_purchase_receipt(
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.stock_ledger import NegativeStockError
item_code = "Backdated-Reco-Cancellation-Item"
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
create_item(item_code)
sr = create_stock_reconciliation(
item_code=item_code,
@ -487,9 +485,8 @@ class TestStockReconciliation(FrappeTestCase):
frappe.flags.dont_execute_stock_reposts = True
frappe.db.rollback()
item_code = "Backdated-Reco-Cancellation-Item"
item_code = make_item().name
warehouse = "_Test Warehouse - _TC"
create_item(item_code)
sr = create_stock_reconciliation(
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 json
from typing import Optional
from typing import Optional, Set, Tuple
import frappe
from frappe import _
@ -214,6 +214,7 @@ def repost_future_sle(
args = get_items_to_be_repost(voucher_type, voucher_no, doc)
distinct_item_warehouses = get_distinct_item_warehouse(args, doc)
affected_transactions = get_affected_transactions(doc)
i = get_current_index(doc) or 0
while i < len(args):
@ -231,6 +232,7 @@ def repost_future_sle(
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
)
affected_transactions.update(obj.affected_transactions)
distinct_item_warehouses[
(args[i].get("item_code"), args[i].get("warehouse"))
@ -250,10 +252,14 @@ def repost_future_sle(
i += 1
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:
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):
@ -263,20 +269,22 @@ def validate_item_warehouse(args):
frappe.throw(_(validation_msg))
def update_args_in_repost_item_valuation(doc, index, args, distinct_item_warehouses):
frappe.db.set_value(
doc.doctype,
doc.name,
def update_args_in_repost_item_valuation(
doc, index, args, distinct_item_warehouses, affected_transactions
):
doc.db_set(
{
"items_to_be_repost": json.dumps(args, default=str),
"distinct_item_and_warehouse": json.dumps(
{str(k): v for k, v in distinct_item_warehouses.items()}, default=str
),
"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(
"item_reposting_progress",
@ -313,6 +321,14 @@ def get_distinct_item_warehouse(args=None, doc=None):
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):
if doc and doc.current_index:
return doc.current_index
@ -360,6 +376,7 @@ class update_entries_after(object):
self.new_items_found = False
self.distinct_item_warehouses = args.get("distinct_item_warehouses", frappe._dict())
self.affected_transactions: Set[Tuple[str, str]] = set()
self.data = frappe._dict()
self.initialize_previous_data(self.args)
@ -518,6 +535,7 @@ class update_entries_after(object):
# previous sle data for this 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):
# 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,
Jan,Jan.,
Feb,Feb.,
Mar,Beschädigen,
Mar,Mrz.,
Apr,Apr.,
Aug,Aug.,
Sep,Sep.,

Can't render this file because it is too large.

File diff suppressed because it is too large Load Diff