Merge pull request #31725 from frappe/version-13-hotfix
chore: weekly version-13 release
This commit is contained in:
@@ -305,7 +305,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def validate_reference_documents(self):
|
||||
if self.party_type == "Student":
|
||||
valid_reference_doctypes = "Fees"
|
||||
valid_reference_doctypes = ("Fees", "Journal Entry")
|
||||
elif self.party_type == "Customer":
|
||||
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
|
||||
elif self.party_type == "Supplier":
|
||||
|
||||
@@ -9,7 +9,7 @@ from frappe import _
|
||||
from frappe.core.page.background_jobs.background_jobs import get_info
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import map_child_doc, map_doc
|
||||
from frappe.utils import flt, getdate, nowdate
|
||||
from frappe.utils import cint, flt, getdate, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
from frappe.utils.scheduler import is_scheduler_inactive
|
||||
|
||||
@@ -219,6 +219,9 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.taxes_and_charges = None
|
||||
invoice.ignore_pricing_rule = 1
|
||||
invoice.customer = self.customer
|
||||
invoice.disable_rounded_total = cint(
|
||||
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
|
||||
)
|
||||
|
||||
if self.merge_invoices_based_on == "Customer Group":
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"account_for_change_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break_23",
|
||||
"income_account",
|
||||
"expense_account",
|
||||
@@ -358,6 +359,13 @@
|
||||
"fieldname": "validate_stock_on_save",
|
||||
"fieldtype": "Check",
|
||||
"label": "Validate Stock on Save"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "If enabled, the consolidated invoices will have rounded total disabled",
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -385,7 +393,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2022-03-21 13:29:28.480533",
|
||||
"modified": "2022-07-21 11:16:46.911173",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
||||
@@ -158,6 +158,7 @@ class PurchaseInvoice(BuyingController):
|
||||
if tds_category and not for_validate:
|
||||
self.apply_tds = 1
|
||||
self.tax_withholding_category = tds_category
|
||||
self.set_onload("supplier_tds", tds_category)
|
||||
|
||||
super(PurchaseInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
|
||||
@@ -414,7 +414,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.is_return && doc.return_against",
|
||||
"depends_on": "eval: doc.is_return",
|
||||
"fieldname": "update_billed_amount_in_sales_order",
|
||||
"fieldtype": "Check",
|
||||
"hide_days": 1,
|
||||
@@ -2046,7 +2046,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-06-16 16:22:44.870575",
|
||||
"modified": "2022-07-11 17:43:56.435382",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
||||
@@ -2712,6 +2712,19 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10)
|
||||
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = ""
|
||||
si.items[1].price_list_rate = 15
|
||||
si.items[1].discount_amount = -5
|
||||
si.items[1].rate = 20
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
|
||||
|
||||
def test_einvoice_without_discounts(self):
|
||||
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
|
||||
|
||||
@@ -2804,6 +2817,19 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 18)
|
||||
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5)
|
||||
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.apply_discount_on = ""
|
||||
si.items[1].price_list_rate = 15
|
||||
si.items[1].discount_amount = -5
|
||||
si.items[1].rate = 20
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
self.assertEqual(einvoice["ItemList"][1]["Discount"], 0)
|
||||
self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20)
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
|
||||
@@ -614,13 +614,13 @@ class SellingController(StockController):
|
||||
stock_items = [d.item_code, d.description, d.warehouse, ""]
|
||||
non_stock_items = [d.item_code, d.description]
|
||||
|
||||
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
|
||||
duplicate_items_msg += "<br><br>"
|
||||
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
|
||||
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
|
||||
duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
|
||||
duplicate_items_msg += "<br><br>"
|
||||
duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
|
||||
frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
|
||||
get_link_to_form("Selling Settings", "Selling Settings"),
|
||||
)
|
||||
if stock_items in check_list:
|
||||
frappe.throw(duplicate_items_msg)
|
||||
else:
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "website_item.image",
|
||||
"fetch_from": "website_item.website_image",
|
||||
"fieldname": "website_item_image",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Website Item Image",
|
||||
@@ -75,7 +75,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-13 21:02:19.031652",
|
||||
"modified": "2022-06-28 16:44:24.718728",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Recommended Items",
|
||||
@@ -83,5 +83,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -30,10 +30,6 @@ frappe.ui.form.on('Website Item', {
|
||||
}, __("View"));
|
||||
},
|
||||
|
||||
image: () => {
|
||||
refresh_field("image_view");
|
||||
},
|
||||
|
||||
copy_from_item_group: (frm) => {
|
||||
return frm.call({
|
||||
doc: frm.doc,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"column_break_11",
|
||||
"description",
|
||||
"brand",
|
||||
"image",
|
||||
"display_section",
|
||||
"website_image",
|
||||
"website_image_alt",
|
||||
@@ -113,8 +112,11 @@
|
||||
{
|
||||
"description": "Item Image (if not slideshow)",
|
||||
"fieldname": "website_image",
|
||||
"fieldtype": "Attach",
|
||||
"label": "Website Image"
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Website Image",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"description": "Image Alternative Text",
|
||||
@@ -188,14 +190,6 @@
|
||||
"options": "Item Group",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "image",
|
||||
"fieldtype": "Attach Image",
|
||||
"hidden": 1,
|
||||
"in_preview": 1,
|
||||
"label": "Image",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "published",
|
||||
@@ -348,13 +342,14 @@
|
||||
}
|
||||
],
|
||||
"has_web_view": 1,
|
||||
"image_field": "image",
|
||||
"image_field": "website_image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-02 13:08:41.942726",
|
||||
"modified": "2022-06-28 17:10:30.613251",
|
||||
"modified_by": "Administrator",
|
||||
"module": "E-commerce",
|
||||
"name": "Website Item",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@@ -410,6 +405,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "web_item_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -1,7 +1,11 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
from typing import TYPE_CHECKING, List, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from erpnext.stock.doctype.item.item import Item
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -116,11 +120,6 @@ class WebsiteItem(WebsiteGenerator):
|
||||
if frappe.flags.in_import:
|
||||
return
|
||||
|
||||
auto_set_website_image = False
|
||||
if not self.website_image and self.image:
|
||||
auto_set_website_image = True
|
||||
self.website_image = self.image
|
||||
|
||||
if not self.website_image:
|
||||
return
|
||||
|
||||
@@ -137,18 +136,16 @@ class WebsiteItem(WebsiteGenerator):
|
||||
file_doc = file_doc[0]
|
||||
|
||||
if not file_doc:
|
||||
if not auto_set_website_image:
|
||||
frappe.msgprint(
|
||||
_("Website Image {0} attached to Item {1} cannot be found").format(
|
||||
self.website_image, self.name
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Website Image {0} attached to Item {1} cannot be found").format(
|
||||
self.website_image, self.name
|
||||
)
|
||||
)
|
||||
|
||||
self.website_image = None
|
||||
|
||||
elif file_doc.is_private:
|
||||
if not auto_set_website_image:
|
||||
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
||||
frappe.msgprint(_("Website Image should be a public file or website URL"))
|
||||
|
||||
self.website_image = None
|
||||
|
||||
@@ -159,9 +156,8 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
if not self.is_new() and self.website_image != frappe.db.get_value(
|
||||
self.doctype, self.name, "website_image"
|
||||
):
|
||||
db_website_image = frappe.db.get_value(self.doctype, self.name, "website_image")
|
||||
if not self.is_new() and self.website_image != db_website_image:
|
||||
self.thumbnail = None
|
||||
|
||||
if self.website_image and not self.thumbnail:
|
||||
@@ -437,7 +433,9 @@ def check_if_user_is_customer(user=None):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_website_item(doc, save=True):
|
||||
def make_website_item(doc: "Item", save: bool = True) -> Union["WebsiteItem", List[str]]:
|
||||
"Make Website Item from Item. Used via Form UI or patch."
|
||||
|
||||
if not doc:
|
||||
return
|
||||
|
||||
@@ -457,7 +455,6 @@ def make_website_item(doc, save=True):
|
||||
"item_group",
|
||||
"stock_uom",
|
||||
"brand",
|
||||
"image",
|
||||
"has_variants",
|
||||
"variant_of",
|
||||
"description",
|
||||
@@ -465,6 +462,10 @@ def make_website_item(doc, save=True):
|
||||
for field in fields_to_map:
|
||||
website_item.update({field: doc.get(field)})
|
||||
|
||||
# Needed for publishing/mapping via Form UI only
|
||||
if not frappe.flags.in_migrate and (doc.get("image") and not website_item.website_image):
|
||||
website_item.website_image = doc.get("image")
|
||||
|
||||
if not save:
|
||||
return website_item
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
frappe.listview_settings['Website Item'] = {
|
||||
add_fields: ["item_name", "web_item_name", "published", "image", "has_variants", "variant_of"],
|
||||
add_fields: ["item_name", "web_item_name", "published", "website_image", "has_variants", "variant_of"],
|
||||
filters: [["published", "=", "1"]],
|
||||
|
||||
get_indicator: function(doc) {
|
||||
|
||||
@@ -20,7 +20,15 @@ def add_to_wishlist(item_code):
|
||||
web_item_data = frappe.db.get_value(
|
||||
"Website Item",
|
||||
{"item_code": item_code},
|
||||
["image", "website_warehouse", "name", "web_item_name", "item_name", "item_group", "route"],
|
||||
[
|
||||
"website_image",
|
||||
"website_warehouse",
|
||||
"name",
|
||||
"web_item_name",
|
||||
"item_name",
|
||||
"item_group",
|
||||
"route",
|
||||
],
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
@@ -30,7 +38,7 @@ def add_to_wishlist(item_code):
|
||||
"item_group": web_item_data.get("item_group"),
|
||||
"website_item": web_item_data.get("name"),
|
||||
"web_item_name": web_item_data.get("web_item_name"),
|
||||
"image": web_item_data.get("image"),
|
||||
"image": web_item_data.get("website_image"),
|
||||
"warehouse": web_item_data.get("website_warehouse"),
|
||||
"route": web_item_data.get("route"),
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ class ProductQuery:
|
||||
"variant_of",
|
||||
"has_variants",
|
||||
"item_group",
|
||||
"image",
|
||||
"web_long_description",
|
||||
"short_description",
|
||||
"route",
|
||||
|
||||
@@ -35,7 +35,7 @@ erpnext.ProductGrid = class {
|
||||
}
|
||||
|
||||
get_image_html(item, title) {
|
||||
let image = item.website_image || item.image;
|
||||
let image = item.website_image;
|
||||
|
||||
if (image) {
|
||||
return `
|
||||
|
||||
@@ -35,7 +35,7 @@ erpnext.ProductList = class {
|
||||
}
|
||||
|
||||
get_image_html(item, title, settings) {
|
||||
let image = item.website_image || item.image;
|
||||
let image = item.website_image;
|
||||
let wishlist_enabled = !item.has_variants && settings.enable_wishlist;
|
||||
let image_html = ``;
|
||||
|
||||
|
||||
@@ -199,16 +199,32 @@ def get_fee_components(fee_structure):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_fee_schedule(program, student_category=None):
|
||||
def get_fee_schedule(program, student_category=None, academic_year=None):
|
||||
"""Returns Fee Schedule.
|
||||
|
||||
:param program: Program.
|
||||
:param student_category: Student Category
|
||||
:param student_category: Student Category.
|
||||
:param academic_year: Academic Year.
|
||||
"""
|
||||
fs = frappe.get_all(
|
||||
"Program Fee",
|
||||
fields=["academic_term", "fee_structure", "due_date", "amount"],
|
||||
filters={"parent": program, "student_category": student_category},
|
||||
filters = {}
|
||||
if program:
|
||||
filters = {"program": program}
|
||||
|
||||
if student_category:
|
||||
filters["student_category"] = student_category
|
||||
|
||||
if academic_year:
|
||||
filters["academic_year"] = academic_year
|
||||
|
||||
fs = frappe.db.get_list(
|
||||
"Fee Schedule",
|
||||
filters=filters,
|
||||
fields=[
|
||||
"academic_term",
|
||||
"fee_structure",
|
||||
"student_category",
|
||||
"due_date",
|
||||
"total_amount as amount",
|
||||
],
|
||||
order_by="idx",
|
||||
)
|
||||
return fs
|
||||
|
||||
@@ -60,12 +60,15 @@ frappe.ui.form.on('Program Enrollment', {
|
||||
method: 'erpnext.education.api.get_fee_schedule',
|
||||
args: {
|
||||
'program': frm.doc.program,
|
||||
'student_category': frm.doc.student_category
|
||||
'student_category': frm.doc.student_category,
|
||||
'academic_year': frm.doc.academic_year
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
cur_frm.clear_table("fees");
|
||||
frm.refresh_fields('fees');
|
||||
frm.set_value('fees' ,r.message);
|
||||
frm.events.get_courses(frm);
|
||||
frm.refresh_fields('fees');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -76,6 +79,10 @@ frappe.ui.form.on('Program Enrollment', {
|
||||
frappe.ui.form.trigger('Program Enrollment', 'program');
|
||||
},
|
||||
|
||||
academic_year: function() {
|
||||
frappe.ui.form.trigger('Program Enrollment', 'program');
|
||||
},
|
||||
|
||||
get_courses: function(frm) {
|
||||
frm.set_value('courses',[]);
|
||||
frappe.call({
|
||||
|
||||
@@ -105,6 +105,8 @@ class ProgramEnrollment(Document):
|
||||
"academic_term": d.academic_term,
|
||||
"fee_structure": d.fee_structure,
|
||||
"program": self.program,
|
||||
"student_batch": self.student_batch_name,
|
||||
"student_category": self.student_category,
|
||||
"due_date": d.due_date,
|
||||
"student_name": self.student_name,
|
||||
"program_enrollment": self.name,
|
||||
|
||||
@@ -305,12 +305,11 @@ class ExpenseClaim(AccountsController):
|
||||
|
||||
if self.total_advance_amount:
|
||||
precision = self.precision("total_advance_amount")
|
||||
if flt(self.total_advance_amount, precision) > flt(self.total_claimed_amount, precision):
|
||||
frappe.throw(_("Total advance amount cannot be greater than total claimed amount"))
|
||||
amount_with_taxes = flt(self.total_sanctioned_amount, precision) + flt(
|
||||
self.total_taxes_and_charges, precision
|
||||
)
|
||||
|
||||
if self.total_sanctioned_amount and flt(self.total_advance_amount, precision) > flt(
|
||||
self.total_sanctioned_amount, precision
|
||||
):
|
||||
if flt(self.total_advance_amount, precision) > amount_with_taxes:
|
||||
frappe.throw(_("Total advance amount cannot be greater than total sanctioned amount"))
|
||||
|
||||
def validate_sanctioned_amount(self):
|
||||
|
||||
@@ -114,6 +114,40 @@ class TestExpenseClaim(FrappeTestCase):
|
||||
self.assertEqual(claim.grand_total, 0)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_advance_amount_allocation_against_claim_with_taxes(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
make_employee_advance,
|
||||
make_payment_entry,
|
||||
)
|
||||
|
||||
frappe.db.delete("Employee Advance")
|
||||
|
||||
payable_account = get_payable_account("_Test Company")
|
||||
taxes = generate_taxes("_Test Company")
|
||||
claim = make_expense_claim(
|
||||
payable_account,
|
||||
700,
|
||||
700,
|
||||
"_Test Company",
|
||||
"Travel Expenses - _TC",
|
||||
do_not_submit=True,
|
||||
taxes=taxes,
|
||||
)
|
||||
claim.save()
|
||||
|
||||
advance = make_employee_advance(claim.employee)
|
||||
pe = make_payment_entry(advance)
|
||||
pe.submit()
|
||||
|
||||
# claim for already paid out advances
|
||||
claim = get_advances_for_claim(claim, advance.name, 763)
|
||||
claim.save()
|
||||
claim.submit()
|
||||
|
||||
self.assertEqual(claim.grand_total, 0)
|
||||
self.assertEqual(claim.status, "Paid")
|
||||
|
||||
def test_expense_claim_partially_paid_via_advance(self):
|
||||
from erpnext.hr.doctype.employee_advance.test_employee_advance import (
|
||||
get_advances_for_claim,
|
||||
@@ -300,12 +334,13 @@ def get_payable_account(company):
|
||||
return frappe.get_cached_value("Company", company, "default_payable_account")
|
||||
|
||||
|
||||
def generate_taxes():
|
||||
def generate_taxes(company=None):
|
||||
company = company or company_name
|
||||
parent_account = frappe.db.get_value(
|
||||
"Account", {"company": company_name, "is_group": 1, "account_type": "Tax"}, "name"
|
||||
"Account", filters={"account_name": "Duties and Taxes", "company": company}
|
||||
)
|
||||
account = create_account(
|
||||
company=company_name,
|
||||
company=company,
|
||||
account_name="Output Tax CGST",
|
||||
account_type="Tax",
|
||||
parent_account=parent_account,
|
||||
|
||||
@@ -21,13 +21,18 @@ frappe.ui.form.on('Member', {
|
||||
|
||||
// custom buttons
|
||||
frm.add_custom_button(__('Accounting Ledger'), function() {
|
||||
frappe.set_route('query-report', 'General Ledger',
|
||||
{party_type:'Member', party:frm.doc.name});
|
||||
if (frm.doc.customer) {
|
||||
frappe.set_route('query-report', 'General Ledger', {party_type: 'Customer', party: frm.doc.customer});
|
||||
} else {
|
||||
frappe.set_route('query-report', 'General Ledger', {party_type: 'Member', party: frm.doc.name});
|
||||
}
|
||||
});
|
||||
|
||||
frm.add_custom_button(__('Accounts Receivable'), function() {
|
||||
frappe.set_route('query-report', 'Accounts Receivable', {member:frm.doc.name});
|
||||
});
|
||||
if (frm.doc.customer) {
|
||||
frm.add_custom_button(__('Accounts Receivable'), function() {
|
||||
frappe.set_route('query-report', 'Accounts Receivable', {customer: frm.doc.customer});
|
||||
});
|
||||
}
|
||||
|
||||
if (!frm.doc.customer) {
|
||||
frm.add_custom_button(__('Create Customer'), () => {
|
||||
|
||||
@@ -371,3 +371,4 @@ erpnext.patches.v13_0.add_cost_center_in_loans
|
||||
erpnext.patches.v13_0.show_india_localisation_deprecation_warning
|
||||
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
|
||||
erpnext.patches.v13_0.reset_corrupt_defaults
|
||||
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning
|
||||
|
||||
@@ -17,7 +17,6 @@ def execute():
|
||||
"item_group",
|
||||
"stock_uom",
|
||||
"brand",
|
||||
"image",
|
||||
"has_variants",
|
||||
"variant_of",
|
||||
"description",
|
||||
@@ -30,6 +29,7 @@ def execute():
|
||||
"website_warehouse",
|
||||
"web_long_description",
|
||||
"website_content",
|
||||
"website_image",
|
||||
"thumbnail",
|
||||
]
|
||||
|
||||
|
||||
16
erpnext/patches/v13_0/show_hr_payroll_deprecation_warning.py
Normal file
16
erpnext/patches/v13_0/show_hr_payroll_deprecation_warning.py
Normal file
@@ -0,0 +1,16 @@
|
||||
import click
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
if "hrms" in frappe.get_installed_apps():
|
||||
return
|
||||
|
||||
click.secho(
|
||||
"HR and Payroll modules have been moved to a separate app"
|
||||
" and will be removed from ERPNext in Version 14."
|
||||
" Please install the HRMS app when upgrading to Version 14"
|
||||
" to continue using the HR and Payroll modules:\n"
|
||||
"https://github.com/frappe/hrms",
|
||||
fg="yellow",
|
||||
)
|
||||
@@ -623,9 +623,20 @@ class SalarySlip(TransactionBase):
|
||||
|
||||
def add_structure_components(self, component_type):
|
||||
data = self.get_data_for_eval()
|
||||
timesheet_component = frappe.db.get_value(
|
||||
"Salary Structure", self.salary_structure, "salary_component"
|
||||
)
|
||||
|
||||
for struct_row in self._salary_structure_doc.get(component_type):
|
||||
if self.salary_slip_based_on_timesheet and struct_row.salary_component == timesheet_component:
|
||||
continue
|
||||
|
||||
amount = self.eval_condition_and_formula(struct_row, data)
|
||||
if amount is not None and struct_row.statistical_component == 0:
|
||||
if (
|
||||
amount
|
||||
or (struct_row.amount_based_on_formula and amount is not None)
|
||||
and struct_row.statistical_component == 0
|
||||
):
|
||||
self.update_component_row(struct_row, amount, component_type, data=data)
|
||||
|
||||
def get_data_for_eval(self):
|
||||
@@ -1352,23 +1363,22 @@ class SalarySlip(TransactionBase):
|
||||
self.total_interest_amount = 0
|
||||
self.total_principal_amount = 0
|
||||
|
||||
if not self.get("loans"):
|
||||
for loan in self.get_loan_details():
|
||||
self.set("loans", [])
|
||||
for loan in self.get_loan_details():
|
||||
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
|
||||
|
||||
amounts = calculate_amounts(loan.name, self.posting_date, "Regular Payment")
|
||||
|
||||
if amounts["interest_amount"] or amounts["payable_principal_amount"]:
|
||||
self.append(
|
||||
"loans",
|
||||
{
|
||||
"loan": loan.name,
|
||||
"total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"],
|
||||
"interest_amount": amounts["interest_amount"],
|
||||
"principal_amount": amounts["payable_principal_amount"],
|
||||
"loan_account": loan.loan_account,
|
||||
"interest_income_account": loan.interest_income_account,
|
||||
},
|
||||
)
|
||||
if amounts["interest_amount"] or amounts["payable_principal_amount"]:
|
||||
self.append(
|
||||
"loans",
|
||||
{
|
||||
"loan": loan.name,
|
||||
"total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"],
|
||||
"interest_amount": amounts["interest_amount"],
|
||||
"principal_amount": amounts["payable_principal_amount"],
|
||||
"loan_account": loan.loan_account,
|
||||
"interest_income_account": loan.interest_income_account,
|
||||
},
|
||||
)
|
||||
|
||||
for payment in self.get("loans"):
|
||||
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")
|
||||
|
||||
@@ -20,6 +20,7 @@ class SalaryStructure(Document):
|
||||
self.validate_max_benefits_with_flexi()
|
||||
self.validate_component_based_on_tax_slab()
|
||||
self.validate_payment_days_based_dependent_component()
|
||||
self.validate_timesheet_component()
|
||||
|
||||
def set_missing_values(self):
|
||||
overwritten_fields = [
|
||||
@@ -89,6 +90,21 @@ class SalaryStructure(Document):
|
||||
|
||||
return abbr
|
||||
|
||||
def validate_timesheet_component(self):
|
||||
if not self.salary_slip_based_on_timesheet:
|
||||
return
|
||||
|
||||
for component in self.earnings:
|
||||
if component.salary_component == self.salary_component:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row #{0}: Timesheet amount will overwrite the Earning component amount for the Salary Component {1}"
|
||||
).format(self.idx, frappe.bold(self.salary_component)),
|
||||
title=_("Warning"),
|
||||
indicator="orange",
|
||||
)
|
||||
break
|
||||
|
||||
def strip_condition_and_formula_fields(self):
|
||||
# remove whitespaces from condition and formula fields
|
||||
for row in self.earnings:
|
||||
|
||||
@@ -16,7 +16,7 @@ class Homepage(Document):
|
||||
def setup_items(self):
|
||||
for d in frappe.get_all(
|
||||
"Website Item",
|
||||
fields=["name", "item_name", "description", "image", "route"],
|
||||
fields=["name", "item_name", "description", "website_image", "route"],
|
||||
filters={"published": 1},
|
||||
limit=3,
|
||||
):
|
||||
@@ -31,7 +31,7 @@ class Homepage(Document):
|
||||
item_code=d.name,
|
||||
item_name=d.item_name,
|
||||
description=d.description,
|
||||
image=d.image,
|
||||
image=d.website_image,
|
||||
route=d.route,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -2122,7 +2122,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
"qty": item.qty,
|
||||
"description": item.description,
|
||||
"serial_no": item.serial_no,
|
||||
"batch_no": item.batch_no
|
||||
"batch_no": item.batch_no,
|
||||
"sample_size": item.sample_quantity
|
||||
});
|
||||
dialog_items.grid.refresh();
|
||||
}
|
||||
|
||||
@@ -265,6 +265,10 @@ def get_overseas_address_details(address_name):
|
||||
def get_item_list(invoice):
|
||||
item_list = []
|
||||
|
||||
hide_discount_in_einvoice = cint(
|
||||
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
|
||||
)
|
||||
|
||||
for d in invoice.items:
|
||||
einvoice_item_schema = read_json("einv_item_template")
|
||||
item = frappe._dict({})
|
||||
@@ -276,17 +280,12 @@ def get_item_list(invoice):
|
||||
item.qty = abs(item.qty)
|
||||
item_qty = item.qty
|
||||
|
||||
item.discount_amount = abs(item.discount_amount)
|
||||
item.taxable_value = abs(item.taxable_value)
|
||||
|
||||
if invoice.get("is_return") or invoice.get("is_debit_note"):
|
||||
item_qty = item_qty or 1
|
||||
|
||||
hide_discount_in_einvoice = cint(
|
||||
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
|
||||
)
|
||||
|
||||
if hide_discount_in_einvoice:
|
||||
if hide_discount_in_einvoice or invoice.is_internal_customer or item.discount_amount < 0:
|
||||
item.unit_rate = item.taxable_value / item_qty
|
||||
item.gross_amount = item.taxable_value
|
||||
item.discount_amount = 0
|
||||
|
||||
@@ -580,7 +580,7 @@ def get_ewb_data(dt, dn):
|
||||
|
||||
if dt == "Delivery Note":
|
||||
data.subSupplyType = 1
|
||||
elif doc.gst_category in ["Registered Regular", "SEZ"]:
|
||||
elif doc.gst_category in ["Unregistered", "Registered Regular", "SEZ"]:
|
||||
data.subSupplyType = 1
|
||||
elif doc.gst_category in ["Overseas", "Deemed Export"]:
|
||||
data.subSupplyType = 3
|
||||
|
||||
@@ -33,7 +33,7 @@ def _execute(filters=None):
|
||||
added_item = []
|
||||
for d in item_list:
|
||||
if (d.parent, d.item_code) not in added_item:
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate]
|
||||
row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate or 0]
|
||||
total_tax = 0
|
||||
for tax in tax_columns:
|
||||
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
|
||||
@@ -100,31 +100,51 @@ def get_items(filters):
|
||||
|
||||
items = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
SELECT
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.stock_uom,
|
||||
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
|
||||
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
|
||||
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
|
||||
sum(
|
||||
`tabSales Invoice Item`.stock_qty
|
||||
) as stock_qty,
|
||||
sum(
|
||||
`tabSales Invoice Item`.base_net_amount
|
||||
) as base_net_amount,
|
||||
sum(
|
||||
`tabSales Invoice Item`.base_price_list_rate
|
||||
) as base_price_list_rate,
|
||||
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code,
|
||||
`tabGST HSN Code`.description,
|
||||
json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail,
|
||||
concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate
|
||||
from
|
||||
`tabSales Invoice`,
|
||||
`tabSales Invoice Item`,
|
||||
`tabGST HSN Code`,
|
||||
`tabSales Taxes and Charges`
|
||||
where
|
||||
`tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
|
||||
and `tabSales Invoice`.docstatus = 1
|
||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
||||
group by
|
||||
json_extract(
|
||||
`tabSales Taxes and Charges`.item_wise_tax_detail,
|
||||
concat(
|
||||
'$."', `tabSales Invoice Item`.item_code,
|
||||
'"[0]'
|
||||
)
|
||||
) * count(
|
||||
distinct `tabSales Taxes and Charges`.name
|
||||
) as tax_rate
|
||||
FROM
|
||||
`tabSales Invoice`
|
||||
INNER JOIN
|
||||
`tabSales Invoice Item` ON `tabSales Invoice`.name = `tabSales Invoice Item`.parent
|
||||
INNER JOIN
|
||||
`tabGST HSN Code` ON `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name % s % s
|
||||
LEFT JOIN
|
||||
`tabSales Taxes and Charges` ON `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name
|
||||
WHERE
|
||||
`tabSales Invoice`.docstatus = 1
|
||||
AND
|
||||
`tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||
GROUP BY
|
||||
`tabSales Invoice Item`.parent,
|
||||
`tabSales Invoice Item`.item_code
|
||||
`tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.uom
|
||||
ORDER BY
|
||||
`tabSales Invoice Item`.gst_hsn_code,
|
||||
`tabSales Invoice Item`.uom
|
||||
"""
|
||||
% (conditions, match_conditions),
|
||||
filters,
|
||||
|
||||
@@ -1548,6 +1548,65 @@ class TestSalesOrder(FrappeTestCase):
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.billing_status, "Fully Billed")
|
||||
|
||||
def test_so_billing_status_with_crnote_against_sales_return(self):
|
||||
"""
|
||||
| Step | Document creation | |
|
||||
|------+--------------------------------------+-------------------------------|
|
||||
| 1 | SO -> DN -> SI | SO Fully Billed and Completed |
|
||||
| 2 | DN -> Sales Return(Partial) | SO 50% Delivered, 100% billed |
|
||||
| 3 | Sales Return(Partial) -> Credit Note | SO 50% Delivered, 50% billed |
|
||||
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
|
||||
so = make_sales_order(uom="Nos", do_not_save=1)
|
||||
so.save()
|
||||
so.submit()
|
||||
|
||||
self.assertEqual(so.billing_status, "Not Billed")
|
||||
|
||||
dn1 = make_delivery_note(so.name)
|
||||
dn1.taxes_and_charges = ""
|
||||
dn1.taxes.clear()
|
||||
dn1.save().submit()
|
||||
|
||||
si = create_sales_invoice(qty=10, do_not_save=1)
|
||||
si.items[0].sales_order = so.name
|
||||
si.items[0].so_detail = so.items[0].name
|
||||
si.items[0].delivery_note = dn1.name
|
||||
si.items[0].dn_detail = dn1.items[0].name
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.billing_status, "Fully Billed")
|
||||
self.assertEqual(so.status, "Completed")
|
||||
|
||||
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
|
||||
|
||||
dn1.reload()
|
||||
dn_ret = create_delivery_note(is_return=1, return_against=dn1.name, qty=-5, do_not_submit=True)
|
||||
dn_ret.items[0].against_sales_order = so.name
|
||||
dn_ret.items[0].so_detail = so.items[0].name
|
||||
dn_ret.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.per_billed, 100)
|
||||
self.assertEqual(so.per_delivered, 50)
|
||||
|
||||
cr_note = create_sales_invoice(is_return=1, qty=-1, do_not_submit=True)
|
||||
cr_note.items[0].qty = -5
|
||||
cr_note.items[0].sales_order = so.name
|
||||
cr_note.items[0].so_detail = so.items[0].name
|
||||
cr_note.items[0].delivery_note = dn_ret.name
|
||||
cr_note.items[0].dn_detail = dn_ret.items[0].name
|
||||
cr_note.update_billed_amount_in_sales_order = True
|
||||
cr_note.submit()
|
||||
|
||||
so.reload()
|
||||
self.assertEqual(so.per_billed, 50)
|
||||
self.assertEqual(so.per_delivered, 50)
|
||||
|
||||
def test_so_back_updated_from_wo_via_mr(self):
|
||||
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import (
|
||||
|
||||
@@ -497,7 +497,10 @@ erpnext.PointOfSale.Controller = class {
|
||||
|
||||
set_pos_profile_data() {
|
||||
if (this.company && !this.frm.doc.company) this.frm.doc.company = this.company;
|
||||
if (this.pos_profile && !this.frm.doc.pos_profile) this.frm.doc.pos_profile = this.pos_profile;
|
||||
if ((this.pos_profile && !this.frm.doc.pos_profile) | (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile)) {
|
||||
this.frm.doc.pos_profile = this.pos_profile;
|
||||
}
|
||||
|
||||
if (!this.frm.doc.company) return;
|
||||
|
||||
return this.frm.trigger("set_pos_data");
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"company",
|
||||
"purpose",
|
||||
"customer",
|
||||
"customer_name",
|
||||
"work_order",
|
||||
"material_request",
|
||||
"for_qty",
|
||||
@@ -126,11 +127,19 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Group Same Items",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.purpose==='Delivery' && doc.customer",
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-21 07:56:40.646473",
|
||||
"modified": "2022-07-19 11:03:04.442174",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Pick List",
|
||||
@@ -202,4 +211,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
})
|
||||
</script>
|
||||
{% else %}
|
||||
{{ product_image(doc.website_image or doc.image, alt=doc.website_image_alt or doc.item_name) }}
|
||||
{{ product_image(doc.website_image, alt=doc.website_image_alt or doc.item_name) }}
|
||||
{% endif %}
|
||||
|
||||
<!-- Simple image preview -->
|
||||
|
||||
@@ -74,7 +74,7 @@
|
||||
{%- set col_size = 3 if is_full_width else 4 -%}
|
||||
{%- set title = item.web_item_name or item.item_name or item.item_code -%}
|
||||
{%- set title = title[:50] + "..." if title|len > 50 else title -%}
|
||||
{%- set image = item.website_image or item.image -%}
|
||||
{%- set image = item.website_image -%}
|
||||
{%- set description = item.website_description or item.description-%}
|
||||
|
||||
{% if is_featured %}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
/* csslint ignore:start */
|
||||
{% if homepage.hero_image %}
|
||||
.hero-image {
|
||||
background-image: url("{{ homepage.hero_image }}");
|
||||
background-size: cover;
|
||||
padding: 10rem 0;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
{% block content %}
|
||||
<main>
|
||||
{% if homepage.hero_section_based_on == 'Default' %}
|
||||
<section class="hero-section border-bottom {%if homepage.hero_image%}hero-image{%endif%}">
|
||||
<section class="hero-section border-bottom {%if homepage.hero_image%}hero-image{%endif%}"
|
||||
{% if homepage.hero_image %}
|
||||
style="background-image: url('{{ homepage.hero_image }}');"
|
||||
{%- endif %}
|
||||
>
|
||||
<div class="container py-5">
|
||||
<h1 class="d-none d-sm-block display-4">{{ homepage.tag_line }}</h1>
|
||||
<h1 class="d-block d-sm-none">{{ homepage.tag_line }}</h1>
|
||||
|
||||
@@ -5997,7 +5997,7 @@ CN,CN,
|
||||
DE,DE,
|
||||
ES,ES,
|
||||
FR,FR,
|
||||
IN,IM,
|
||||
IN,Ein,
|
||||
JP,JP,
|
||||
IT,ES,
|
||||
MX,MX,
|
||||
|
||||
|
Can't render this file because it is too large.
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user