Merge pull request #31725 from frappe/version-13-hotfix

chore: weekly version-13 release
This commit is contained in:
Deepesh Garg
2022-07-28 17:42:15 +05:30
committed by GitHub
43 changed files with 10979 additions and 6967 deletions

View File

@@ -305,7 +305,7 @@ class PaymentEntry(AccountsController):
def validate_reference_documents(self): def validate_reference_documents(self):
if self.party_type == "Student": if self.party_type == "Student":
valid_reference_doctypes = "Fees" valid_reference_doctypes = ("Fees", "Journal Entry")
elif self.party_type == "Customer": elif self.party_type == "Customer":
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
elif self.party_type == "Supplier": elif self.party_type == "Supplier":

View File

@@ -9,7 +9,7 @@ from frappe import _
from frappe.core.page.background_jobs.background_jobs import get_info from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc 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.background_jobs import enqueue
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
@@ -219,6 +219,9 @@ class POSInvoiceMergeLog(Document):
invoice.taxes_and_charges = None invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1 invoice.ignore_pricing_rule = 1
invoice.customer = self.customer 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": if self.merge_invoices_based_on == "Customer Group":
invoice.flags.ignore_pos_profile = True invoice.flags.ignore_pos_profile = True

View File

@@ -44,6 +44,7 @@
"write_off_account", "write_off_account",
"write_off_cost_center", "write_off_cost_center",
"account_for_change_amount", "account_for_change_amount",
"disable_rounded_total",
"column_break_23", "column_break_23",
"income_account", "income_account",
"expense_account", "expense_account",
@@ -358,6 +359,13 @@
"fieldname": "validate_stock_on_save", "fieldname": "validate_stock_on_save",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Validate Stock on Save" "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", "icon": "icon-cog",
@@ -385,7 +393,7 @@
"link_fieldname": "pos_profile" "link_fieldname": "pos_profile"
} }
], ],
"modified": "2022-03-21 13:29:28.480533", "modified": "2022-07-21 11:16:46.911173",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Profile", "name": "POS Profile",

View File

@@ -158,6 +158,7 @@ class PurchaseInvoice(BuyingController):
if tds_category and not for_validate: if tds_category and not for_validate:
self.apply_tds = 1 self.apply_tds = 1
self.tax_withholding_category = tds_category self.tax_withholding_category = tds_category
self.set_onload("supplier_tds", tds_category)
super(PurchaseInvoice, self).set_missing_values(for_validate) super(PurchaseInvoice, self).set_missing_values(for_validate)

View File

@@ -414,7 +414,7 @@
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval: doc.is_return && doc.return_against", "depends_on": "eval: doc.is_return",
"fieldname": "update_billed_amount_in_sales_order", "fieldname": "update_billed_amount_in_sales_order",
"fieldtype": "Check", "fieldtype": "Check",
"hide_days": 1, "hide_days": 1,
@@ -2046,7 +2046,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-06-16 16:22:44.870575", "modified": "2022-07-11 17:43:56.435382",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@@ -2712,6 +2712,19 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20) self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10) 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): def test_einvoice_without_discounts(self):
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals 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"][2]["UnitPrice"], 18)
self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5) 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): def test_item_tax_net_range(self):
item = create_item("T Shirt") item = create_item("T Shirt")

View File

@@ -614,13 +614,13 @@ class SellingController(StockController):
stock_items = [d.item_code, d.description, d.warehouse, ""] stock_items = [d.item_code, d.description, d.warehouse, ""]
non_stock_items = [d.item_code, d.description] 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: 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: if stock_items in check_list:
frappe.throw(duplicate_items_msg) frappe.throw(duplicate_items_msg)
else: else:

View File

@@ -48,7 +48,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "website_item.image", "fetch_from": "website_item.website_image",
"fieldname": "website_item_image", "fieldname": "website_item_image",
"fieldtype": "Attach", "fieldtype": "Attach",
"label": "Website Item Image", "label": "Website Item Image",
@@ -75,7 +75,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-07-13 21:02:19.031652", "modified": "2022-06-28 16:44:24.718728",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "E-commerce", "module": "E-commerce",
"name": "Recommended Items", "name": "Recommended Items",
@@ -83,5 +83,6 @@
"permissions": [], "permissions": [],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -30,10 +30,6 @@ frappe.ui.form.on('Website Item', {
}, __("View")); }, __("View"));
}, },
image: () => {
refresh_field("image_view");
},
copy_from_item_group: (frm) => { copy_from_item_group: (frm) => {
return frm.call({ return frm.call({
doc: frm.doc, doc: frm.doc,

View File

@@ -22,7 +22,6 @@
"column_break_11", "column_break_11",
"description", "description",
"brand", "brand",
"image",
"display_section", "display_section",
"website_image", "website_image",
"website_image_alt", "website_image_alt",
@@ -113,8 +112,11 @@
{ {
"description": "Item Image (if not slideshow)", "description": "Item Image (if not slideshow)",
"fieldname": "website_image", "fieldname": "website_image",
"fieldtype": "Attach", "fieldtype": "Attach Image",
"label": "Website Image" "hidden": 1,
"in_preview": 1,
"label": "Website Image",
"print_hide": 1
}, },
{ {
"description": "Image Alternative Text", "description": "Image Alternative Text",
@@ -188,14 +190,6 @@
"options": "Item Group", "options": "Item Group",
"read_only": 1 "read_only": 1
}, },
{
"fieldname": "image",
"fieldtype": "Attach Image",
"hidden": 1,
"in_preview": 1,
"label": "Image",
"print_hide": 1
},
{ {
"default": "1", "default": "1",
"fieldname": "published", "fieldname": "published",
@@ -348,13 +342,14 @@
} }
], ],
"has_web_view": 1, "has_web_view": 1,
"image_field": "image", "image_field": "website_image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-09-02 13:08:41.942726", "modified": "2022-06-28 17:10:30.613251",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "E-commerce", "module": "E-commerce",
"name": "Website Item", "name": "Website Item",
"naming_rule": "Expression (old style)",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -410,6 +405,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"title_field": "web_item_name", "title_field": "web_item_name",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -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 # For license information, please see license.txt
import json import json
from typing import TYPE_CHECKING, List, Union
if TYPE_CHECKING:
from erpnext.stock.doctype.item.item import Item
import frappe import frappe
from frappe import _ from frappe import _
@@ -116,11 +120,6 @@ class WebsiteItem(WebsiteGenerator):
if frappe.flags.in_import: if frappe.flags.in_import:
return 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: if not self.website_image:
return return
@@ -137,18 +136,16 @@ class WebsiteItem(WebsiteGenerator):
file_doc = file_doc[0] file_doc = file_doc[0]
if not file_doc: if not file_doc:
if not auto_set_website_image: frappe.msgprint(
frappe.msgprint( _("Website Image {0} attached to Item {1} cannot be found").format(
_("Website Image {0} attached to Item {1} cannot be found").format( self.website_image, self.name
self.website_image, self.name
)
) )
)
self.website_image = None self.website_image = None
elif file_doc.is_private: 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 self.website_image = None
@@ -159,9 +156,8 @@ class WebsiteItem(WebsiteGenerator):
import requests.exceptions import requests.exceptions
if not self.is_new() and self.website_image != frappe.db.get_value( db_website_image = frappe.db.get_value(self.doctype, self.name, "website_image")
self.doctype, self.name, "website_image" if not self.is_new() and self.website_image != db_website_image:
):
self.thumbnail = None self.thumbnail = None
if self.website_image and not self.thumbnail: if self.website_image and not self.thumbnail:
@@ -437,7 +433,9 @@ def check_if_user_is_customer(user=None):
@frappe.whitelist() @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: if not doc:
return return
@@ -457,7 +455,6 @@ def make_website_item(doc, save=True):
"item_group", "item_group",
"stock_uom", "stock_uom",
"brand", "brand",
"image",
"has_variants", "has_variants",
"variant_of", "variant_of",
"description", "description",
@@ -465,6 +462,10 @@ def make_website_item(doc, save=True):
for field in fields_to_map: for field in fields_to_map:
website_item.update({field: doc.get(field)}) 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: if not save:
return website_item return website_item

View File

@@ -1,5 +1,5 @@
frappe.listview_settings['Website Item'] = { 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"]], filters: [["published", "=", "1"]],
get_indicator: function(doc) { get_indicator: function(doc) {

View File

@@ -20,7 +20,15 @@ def add_to_wishlist(item_code):
web_item_data = frappe.db.get_value( web_item_data = frappe.db.get_value(
"Website Item", "Website Item",
{"item_code": item_code}, {"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, as_dict=1,
) )
@@ -30,7 +38,7 @@ def add_to_wishlist(item_code):
"item_group": web_item_data.get("item_group"), "item_group": web_item_data.get("item_group"),
"website_item": web_item_data.get("name"), "website_item": web_item_data.get("name"),
"web_item_name": web_item_data.get("web_item_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"), "warehouse": web_item_data.get("website_warehouse"),
"route": web_item_data.get("route"), "route": web_item_data.get("route"),
} }

View File

@@ -35,7 +35,6 @@ class ProductQuery:
"variant_of", "variant_of",
"has_variants", "has_variants",
"item_group", "item_group",
"image",
"web_long_description", "web_long_description",
"short_description", "short_description",
"route", "route",

View File

@@ -35,7 +35,7 @@ erpnext.ProductGrid = class {
} }
get_image_html(item, title) { get_image_html(item, title) {
let image = item.website_image || item.image; let image = item.website_image;
if (image) { if (image) {
return ` return `

View File

@@ -35,7 +35,7 @@ erpnext.ProductList = class {
} }
get_image_html(item, title, settings) { 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 wishlist_enabled = !item.has_variants && settings.enable_wishlist;
let image_html = ``; let image_html = ``;

View File

@@ -199,16 +199,32 @@ def get_fee_components(fee_structure):
@frappe.whitelist() @frappe.whitelist()
def get_fee_schedule(program, student_category=None): def get_fee_schedule(program, student_category=None, academic_year=None):
"""Returns Fee Schedule. """Returns Fee Schedule.
:param program: Program. :param program: Program.
:param student_category: Student Category :param student_category: Student Category.
:param academic_year: Academic Year.
""" """
fs = frappe.get_all( filters = {}
"Program Fee", if program:
fields=["academic_term", "fee_structure", "due_date", "amount"], filters = {"program": program}
filters={"parent": program, "student_category": student_category},
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", order_by="idx",
) )
return fs return fs

View File

@@ -60,12 +60,15 @@ frappe.ui.form.on('Program Enrollment', {
method: 'erpnext.education.api.get_fee_schedule', method: 'erpnext.education.api.get_fee_schedule',
args: { args: {
'program': frm.doc.program, '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) { callback: function(r) {
if (r.message) { if (r.message) {
cur_frm.clear_table("fees");
frm.refresh_fields('fees');
frm.set_value('fees' ,r.message); 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'); frappe.ui.form.trigger('Program Enrollment', 'program');
}, },
academic_year: function() {
frappe.ui.form.trigger('Program Enrollment', 'program');
},
get_courses: function(frm) { get_courses: function(frm) {
frm.set_value('courses',[]); frm.set_value('courses',[]);
frappe.call({ frappe.call({

View File

@@ -105,6 +105,8 @@ class ProgramEnrollment(Document):
"academic_term": d.academic_term, "academic_term": d.academic_term,
"fee_structure": d.fee_structure, "fee_structure": d.fee_structure,
"program": self.program, "program": self.program,
"student_batch": self.student_batch_name,
"student_category": self.student_category,
"due_date": d.due_date, "due_date": d.due_date,
"student_name": self.student_name, "student_name": self.student_name,
"program_enrollment": self.name, "program_enrollment": self.name,

View File

@@ -305,12 +305,11 @@ class ExpenseClaim(AccountsController):
if self.total_advance_amount: if self.total_advance_amount:
precision = self.precision("total_advance_amount") precision = self.precision("total_advance_amount")
if flt(self.total_advance_amount, precision) > flt(self.total_claimed_amount, precision): amount_with_taxes = flt(self.total_sanctioned_amount, precision) + flt(
frappe.throw(_("Total advance amount cannot be greater than total claimed amount")) self.total_taxes_and_charges, precision
)
if self.total_sanctioned_amount and flt(self.total_advance_amount, precision) > flt( if flt(self.total_advance_amount, precision) > amount_with_taxes:
self.total_sanctioned_amount, precision
):
frappe.throw(_("Total advance amount cannot be greater than total sanctioned amount")) frappe.throw(_("Total advance amount cannot be greater than total sanctioned amount"))
def validate_sanctioned_amount(self): def validate_sanctioned_amount(self):

View File

@@ -114,6 +114,40 @@ class TestExpenseClaim(FrappeTestCase):
self.assertEqual(claim.grand_total, 0) self.assertEqual(claim.grand_total, 0)
self.assertEqual(claim.status, "Paid") 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): def test_expense_claim_partially_paid_via_advance(self):
from erpnext.hr.doctype.employee_advance.test_employee_advance import ( from erpnext.hr.doctype.employee_advance.test_employee_advance import (
get_advances_for_claim, get_advances_for_claim,
@@ -300,12 +334,13 @@ def get_payable_account(company):
return frappe.get_cached_value("Company", company, "default_payable_account") 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( 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( account = create_account(
company=company_name, company=company,
account_name="Output Tax CGST", account_name="Output Tax CGST",
account_type="Tax", account_type="Tax",
parent_account=parent_account, parent_account=parent_account,

View File

@@ -21,13 +21,18 @@ frappe.ui.form.on('Member', {
// custom buttons // custom buttons
frm.add_custom_button(__('Accounting Ledger'), function() { frm.add_custom_button(__('Accounting Ledger'), function() {
frappe.set_route('query-report', 'General Ledger', if (frm.doc.customer) {
{party_type:'Member', party:frm.doc.name}); 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() { if (frm.doc.customer) {
frappe.set_route('query-report', 'Accounts Receivable', {member:frm.doc.name}); frm.add_custom_button(__('Accounts Receivable'), function() {
}); frappe.set_route('query-report', 'Accounts Receivable', {customer: frm.doc.customer});
});
}
if (!frm.doc.customer) { if (!frm.doc.customer) {
frm.add_custom_button(__('Create Customer'), () => { frm.add_custom_button(__('Create Customer'), () => {

View File

@@ -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.show_india_localisation_deprecation_warning
erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation erpnext.patches.v13_0.fix_number_and_frequency_for_monthly_depreciation
erpnext.patches.v13_0.reset_corrupt_defaults erpnext.patches.v13_0.reset_corrupt_defaults
erpnext.patches.v13_0.show_hr_payroll_deprecation_warning

View File

@@ -17,7 +17,6 @@ def execute():
"item_group", "item_group",
"stock_uom", "stock_uom",
"brand", "brand",
"image",
"has_variants", "has_variants",
"variant_of", "variant_of",
"description", "description",
@@ -30,6 +29,7 @@ def execute():
"website_warehouse", "website_warehouse",
"web_long_description", "web_long_description",
"website_content", "website_content",
"website_image",
"thumbnail", "thumbnail",
] ]

View 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",
)

View File

@@ -623,9 +623,20 @@ class SalarySlip(TransactionBase):
def add_structure_components(self, component_type): def add_structure_components(self, component_type):
data = self.get_data_for_eval() 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): 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) 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) self.update_component_row(struct_row, amount, component_type, data=data)
def get_data_for_eval(self): def get_data_for_eval(self):
@@ -1352,23 +1363,22 @@ class SalarySlip(TransactionBase):
self.total_interest_amount = 0 self.total_interest_amount = 0
self.total_principal_amount = 0 self.total_principal_amount = 0
if not self.get("loans"): self.set("loans", [])
for loan in self.get_loan_details(): 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(
if amounts["interest_amount"] or amounts["payable_principal_amount"]: "loans",
self.append( {
"loans", "loan": loan.name,
{ "total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"],
"loan": loan.name, "interest_amount": amounts["interest_amount"],
"total_payment": amounts["interest_amount"] + amounts["payable_principal_amount"], "principal_amount": amounts["payable_principal_amount"],
"interest_amount": amounts["interest_amount"], "loan_account": loan.loan_account,
"principal_amount": amounts["payable_principal_amount"], "interest_income_account": loan.interest_income_account,
"loan_account": loan.loan_account, },
"interest_income_account": loan.interest_income_account, )
},
)
for payment in self.get("loans"): for payment in self.get("loans"):
amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment") amounts = calculate_amounts(payment.loan, self.posting_date, "Regular Payment")

View File

@@ -20,6 +20,7 @@ class SalaryStructure(Document):
self.validate_max_benefits_with_flexi() self.validate_max_benefits_with_flexi()
self.validate_component_based_on_tax_slab() self.validate_component_based_on_tax_slab()
self.validate_payment_days_based_dependent_component() self.validate_payment_days_based_dependent_component()
self.validate_timesheet_component()
def set_missing_values(self): def set_missing_values(self):
overwritten_fields = [ overwritten_fields = [
@@ -89,6 +90,21 @@ class SalaryStructure(Document):
return abbr 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): def strip_condition_and_formula_fields(self):
# remove whitespaces from condition and formula fields # remove whitespaces from condition and formula fields
for row in self.earnings: for row in self.earnings:

View File

@@ -16,7 +16,7 @@ class Homepage(Document):
def setup_items(self): def setup_items(self):
for d in frappe.get_all( for d in frappe.get_all(
"Website Item", "Website Item",
fields=["name", "item_name", "description", "image", "route"], fields=["name", "item_name", "description", "website_image", "route"],
filters={"published": 1}, filters={"published": 1},
limit=3, limit=3,
): ):
@@ -31,7 +31,7 @@ class Homepage(Document):
item_code=d.name, item_code=d.name,
item_name=d.item_name, item_name=d.item_name,
description=d.description, description=d.description,
image=d.image, image=d.website_image,
route=d.route, route=d.route,
), ),
) )

View File

@@ -2122,7 +2122,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
"qty": item.qty, "qty": item.qty,
"description": item.description, "description": item.description,
"serial_no": item.serial_no, "serial_no": item.serial_no,
"batch_no": item.batch_no "batch_no": item.batch_no,
"sample_size": item.sample_quantity
}); });
dialog_items.grid.refresh(); dialog_items.grid.refresh();
} }

View File

@@ -265,6 +265,10 @@ def get_overseas_address_details(address_name):
def get_item_list(invoice): def get_item_list(invoice):
item_list = [] 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: for d in invoice.items:
einvoice_item_schema = read_json("einv_item_template") einvoice_item_schema = read_json("einv_item_template")
item = frappe._dict({}) item = frappe._dict({})
@@ -276,17 +280,12 @@ def get_item_list(invoice):
item.qty = abs(item.qty) item.qty = abs(item.qty)
item_qty = item.qty item_qty = item.qty
item.discount_amount = abs(item.discount_amount)
item.taxable_value = abs(item.taxable_value) item.taxable_value = abs(item.taxable_value)
if invoice.get("is_return") or invoice.get("is_debit_note"): if invoice.get("is_return") or invoice.get("is_debit_note"):
item_qty = item_qty or 1 item_qty = item_qty or 1
hide_discount_in_einvoice = cint( if hide_discount_in_einvoice or invoice.is_internal_customer or item.discount_amount < 0:
frappe.db.get_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice")
)
if hide_discount_in_einvoice:
item.unit_rate = item.taxable_value / item_qty item.unit_rate = item.taxable_value / item_qty
item.gross_amount = item.taxable_value item.gross_amount = item.taxable_value
item.discount_amount = 0 item.discount_amount = 0

View File

@@ -580,7 +580,7 @@ def get_ewb_data(dt, dn):
if dt == "Delivery Note": if dt == "Delivery Note":
data.subSupplyType = 1 data.subSupplyType = 1
elif doc.gst_category in ["Registered Regular", "SEZ"]: elif doc.gst_category in ["Unregistered", "Registered Regular", "SEZ"]:
data.subSupplyType = 1 data.subSupplyType = 1
elif doc.gst_category in ["Overseas", "Deemed Export"]: elif doc.gst_category in ["Overseas", "Deemed Export"]:
data.subSupplyType = 3 data.subSupplyType = 3

View File

@@ -33,7 +33,7 @@ def _execute(filters=None):
added_item = [] added_item = []
for d in item_list: for d in item_list:
if (d.parent, d.item_code) not in added_item: 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 total_tax = 0
for tax in tax_columns: for tax in tax_columns:
item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {})
@@ -100,31 +100,51 @@ def get_items(filters):
items = frappe.db.sql( items = frappe.db.sql(
""" """
select SELECT
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.gst_hsn_code,
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.stock_uom,
sum(`tabSales Invoice Item`.stock_qty) as stock_qty, sum(
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount, `tabSales Invoice Item`.stock_qty
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate, ) 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`.parent,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description, `tabGST HSN Code`.description,
json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail, json_extract(
concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate `tabSales Taxes and Charges`.item_wise_tax_detail,
from concat(
`tabSales Invoice`, '$."', `tabSales Invoice Item`.item_code,
`tabSales Invoice Item`, '"[0]'
`tabGST HSN Code`, )
`tabSales Taxes and Charges` ) * count(
where distinct `tabSales Taxes and Charges`.name
`tabSales Invoice`.name = `tabSales Invoice Item`.parent ) as tax_rate
and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name FROM
and `tabSales Invoice`.docstatus = 1 `tabSales Invoice`
and `tabSales Invoice Item`.gst_hsn_code is not NULL INNER JOIN
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s `tabSales Invoice Item` ON `tabSales Invoice`.name = `tabSales Invoice Item`.parent
group by 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`.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), % (conditions, match_conditions),
filters, filters,

View File

@@ -1548,6 +1548,65 @@ class TestSalesOrder(FrappeTestCase):
so.load_from_db() so.load_from_db()
self.assertEqual(so.billing_status, "Fully Billed") 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): def test_so_back_updated_from_wo_via_mr(self):
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."
from erpnext.manufacturing.doctype.work_order.work_order import ( from erpnext.manufacturing.doctype.work_order.work_order import (

View File

@@ -497,7 +497,10 @@ erpnext.PointOfSale.Controller = class {
set_pos_profile_data() { set_pos_profile_data() {
if (this.company && !this.frm.doc.company) this.frm.doc.company = this.company; 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; if (!this.frm.doc.company) return;
return this.frm.trigger("set_pos_data"); return this.frm.trigger("set_pos_data");

View File

@@ -10,6 +10,7 @@
"company", "company",
"purpose", "purpose",
"customer", "customer",
"customer_name",
"work_order", "work_order",
"material_request", "material_request",
"for_qty", "for_qty",
@@ -126,11 +127,19 @@
"fieldtype": "Check", "fieldtype": "Check",
"label": "Group Same Items", "label": "Group Same Items",
"print_hide": 1 "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, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-04-21 07:56:40.646473", "modified": "2022-07-19 11:03:04.442174",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Pick List", "name": "Pick List",
@@ -202,4 +211,4 @@
"sort_order": "DESC", "sort_order": "DESC",
"states": [], "states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -24,7 +24,7 @@
}) })
</script> </script>
{% else %} {% 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 %} {% endif %}
<!-- Simple image preview --> <!-- Simple image preview -->

View File

@@ -74,7 +74,7 @@
{%- set col_size = 3 if is_full_width else 4 -%} {%- 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 = item.web_item_name or item.item_name or item.item_code -%}
{%- set title = title[:50] + "..." if title|len > 50 else title -%} {%- 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-%} {%- set description = item.website_description or item.description-%}
{% if is_featured %} {% if is_featured %}

View File

@@ -1,7 +1,6 @@
/* csslint ignore:start */ /* csslint ignore:start */
{% if homepage.hero_image %} {% if homepage.hero_image %}
.hero-image { .hero-image {
background-image: url("{{ homepage.hero_image }}");
background-size: cover; background-size: cover;
padding: 10rem 0; padding: 10rem 0;
} }

View File

@@ -5,7 +5,11 @@
{% block content %} {% block content %}
<main> <main>
{% if homepage.hero_section_based_on == 'Default' %} {% 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"> <div class="container py-5">
<h1 class="d-none d-sm-block display-4">{{ homepage.tag_line }}</h1> <h1 class="d-none d-sm-block display-4">{{ homepage.tag_line }}</h1>
<h1 class="d-block d-sm-none">{{ homepage.tag_line }}</h1> <h1 class="d-block d-sm-none">{{ homepage.tag_line }}</h1>

View File

@@ -5997,7 +5997,7 @@ CN,CN,
DE,DE, DE,DE,
ES,ES, ES,ES,
FR,FR, FR,FR,
IN,IM, IN,Ein,
JP,JP, JP,JP,
IT,ES, IT,ES,
MX,MX, 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