Merge branch 'develop' into sales_purchase_retrun_optimization

This commit is contained in:
Deepesh Garg 2022-03-13 12:45:10 +05:30 committed by GitHub
commit e4e38ad60b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 473 additions and 188 deletions

View File

@ -64,10 +64,10 @@
<td></td> <td></td>
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td> <td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td>
<td style="text-align: right"> <td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
</td> </td>
<td style="text-align: right"> <td style="text-align: right">
{{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
</td> </td>
{% endif %} {% endif %}
<td style="text-align: right"> <td style="text-align: right">

View File

@ -51,6 +51,13 @@ frappe.ui.form.on('Process Statement Of Accounts', {
} }
} }
}); });
frm.set_query("account", function() {
return {
filters: {
'company': frm.doc.company
}
};
});
if(frm.doc.__islocal){ if(frm.doc.__islocal){
frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1)); frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value('to_date', frappe.datetime.get_today()); frm.set_value('to_date', frappe.datetime.get_today());

View File

@ -9,7 +9,6 @@
"customer_section", "customer_section",
"title", "title",
"naming_series", "naming_series",
"tax_invoice_number",
"customer", "customer",
"customer_name", "customer_name",
"tax_id", "tax_id",
@ -2027,12 +2026,6 @@
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Amount Eligible for Commission", "label": "Amount Eligible for Commission",
"read_only": 1 "read_only": 1
},
{
"fieldname": "tax_invoice_number",
"fieldtype": "Data",
"label": "Tax Invoice Number",
"read_only": 1
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -2045,7 +2038,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2022-03-07 16:08:53.517903", "modified": "2022-03-08 16:08:53.517903",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -1255,14 +1255,14 @@ class SalesInvoice(SellingController):
def update_billing_status_in_dn(self, update_modified=True): def update_billing_status_in_dn(self, update_modified=True):
updated_delivery_notes = [] updated_delivery_notes = []
for d in self.get("items"): for d in self.get("items"):
if d.so_detail: if d.dn_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
elif d.dn_detail:
billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item`
where dn_detail=%s and docstatus=1""", d.dn_detail) where dn_detail=%s and docstatus=1""", d.dn_detail)
billed_amt = billed_amt and billed_amt[0][0] or 0 billed_amt = billed_amt and billed_amt[0][0] or 0
frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified)
updated_delivery_notes.append(d.delivery_note) updated_delivery_notes.append(d.delivery_note)
elif d.so_detail:
updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified)
for dn in set(updated_delivery_notes): for dn in set(updated_delivery_notes):
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified) frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)

View File

@ -71,8 +71,7 @@ class ShippingRule(Document):
if doc.currency != doc.company_currency: if doc.currency != doc.company_currency:
shipping_amount = flt(shipping_amount / doc.conversion_rate, 2) shipping_amount = flt(shipping_amount / doc.conversion_rate, 2)
if shipping_amount: self.add_shipping_rule_to_tax_table(doc, shipping_amount)
self.add_shipping_rule_to_tax_table(doc, shipping_amount)
def get_shipping_amount_from_rules(self, value): def get_shipping_amount_from_rules(self, value):
for condition in self.get("conditions"): for condition in self.get("conditions"):

View File

@ -151,7 +151,7 @@ def set_contact_details(party_details, party, party_type):
def set_other_values(party_details, party, party_type): def set_other_values(party_details, party, party_type):
# copy # copy
if party_type=="Customer": if party_type == "Customer":
to_copy = ["customer_name", "customer_group", "territory", "language"] to_copy = ["customer_name", "customer_group", "territory", "language"]
else: else:
to_copy = ["supplier_name", "supplier_group", "language"] to_copy = ["supplier_name", "supplier_group", "language"]
@ -170,12 +170,8 @@ def get_default_price_list(party):
return party.default_price_list return party.default_price_list
if party.doctype == "Customer": if party.doctype == "Customer":
price_list = frappe.get_cached_value("Customer Group", return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list")
party.customer_group, "default_price_list")
if price_list:
return price_list
return None
def set_price_list(party_details, party, party_type, given_price_list, pos=None): def set_price_list(party_details, party, party_type, given_price_list, pos=None):
# price list # price list

View File

@ -0,0 +1,16 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.party import get_default_price_list
class PartyTestCase(FrappeTestCase):
def test_get_default_price_list_should_return_none_for_invalid_group(self):
customer = frappe.get_doc({
'doctype': 'Customer',
'customer_name': 'test customer',
}).insert(ignore_permissions=True, ignore_mandatory=True)
customer.customer_group = None
customer.save()
price_list = get_default_price_list(customer)
assert price_list is None

View File

@ -1,5 +1,6 @@
from frappe import _ from frappe import _
def get_data(): def get_data():
return { return {
'non_standard_fieldnames': { 'non_standard_fieldnames': {

View File

@ -23,7 +23,7 @@ def post_depreciation_entries(date=None):
frappe.db.commit() frappe.db.commit()
def get_depreciable_assets(date): def get_depreciable_assets(date):
return frappe.db.sql_list("""select a.name return frappe.db.sql_list("""select distinct a.name
from tabAsset a, `tabDepreciation Schedule` ds from tabAsset a, `tabDepreciation Schedule` ds
where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1 where a.name = ds.parent and a.docstatus=1 and ds.schedule_date<=%s and a.calculate_depreciation = 1
and a.status in ('Submitted', 'Partially Depreciated') and a.status in ('Submitted', 'Partially Depreciated')

View File

@ -17,8 +17,8 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
def setUp(self) -> None: def setUp(self) -> None:
create_item("Test MR Report Item") create_item("Test MR Report Item")
self.setup_material_request() # to order and receive self.setup_material_request() # to order and receive
self.setup_material_request(order=True) # to receive (ordered) self.setup_material_request(order=True, days=1) # to receive (ordered)
self.setup_material_request(order=True, receive=True) # complete (ordered & received) self.setup_material_request(order=True, receive=True, days=2) # complete (ordered & received)
self.filters = frappe._dict( self.filters = frappe._dict(
company="_Test Company", from_date=today(), to_date=add_days(today(), 30), company="_Test Company", from_date=today(), to_date=add_days(today(), 30),
@ -32,9 +32,8 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
data = get_data(self.filters) data = get_data(self.filters)
self.assertEqual(len(data), 2) # MRs today should be fetched self.assertEqual(len(data), 2) # MRs today should be fetched
self.filters.from_date = add_days(today(), 1) data = get_data(self.filters.update({"from_date": add_days(today(), 10)}))
data = get_data(self.filters) self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is in future
self.assertEqual(len(data), 0) # MRs today should not be fetched as from date is tomorrow
def test_ordered_received_material_requests(self): def test_ordered_received_material_requests(self):
data = get_data(self.filters) data = get_data(self.filters)
@ -44,19 +43,19 @@ class TestRequestedItemsToOrderAndReceive(FrappeTestCase):
self.assertEqual(data[0].ordered_qty, 0.0) self.assertEqual(data[0].ordered_qty, 0.0)
self.assertEqual(data[1].ordered_qty, 57.0) self.assertEqual(data[1].ordered_qty, 57.0)
def setup_material_request(self, order=False, receive=False): def setup_material_request(self, order=False, receive=False, days=0):
po = None po = None
test_records = frappe.get_test_records('Material Request') test_records = frappe.get_test_records('Material Request')
mr = frappe.copy_doc(test_records[0]) mr = frappe.copy_doc(test_records[0])
mr.transaction_date = today() mr.transaction_date = add_days(today(), days)
mr.schedule_date = add_days(today(), 1) mr.schedule_date = add_days(mr.transaction_date, 1)
for row in mr.items: for row in mr.items:
row.item_code = "Test MR Report Item" row.item_code = "Test MR Report Item"
row.item_name = "Test MR Report Item" row.item_name = "Test MR Report Item"
row.description = "Test MR Report Item" row.description = "Test MR Report Item"
row.uom = "Nos" row.uom = "Nos"
row.schedule_date = add_days(today(), 1) row.schedule_date = mr.schedule_date
mr.submit() mr.submit()
if order or receive: if order or receive:

View File

@ -37,6 +37,8 @@ class calculate_taxes_and_totals(object):
self.set_discount_amount() self.set_discount_amount()
self.apply_discount_amount() self.apply_discount_amount()
self.calculate_shipping_charges()
if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]: if self.doc.doctype in ["Sales Invoice", "Purchase Invoice"]:
self.calculate_total_advance() self.calculate_total_advance()
@ -50,7 +52,6 @@ class calculate_taxes_and_totals(object):
self.initialize_taxes() self.initialize_taxes()
self.determine_exclusive_rate() self.determine_exclusive_rate()
self.calculate_net_total() self.calculate_net_total()
self.calculate_shipping_charges()
self.calculate_taxes() self.calculate_taxes()
self.manipulate_grand_total_for_inclusive_tax() self.manipulate_grand_total_for_inclusive_tax()
self.calculate_totals() self.calculate_totals()
@ -276,6 +277,8 @@ class calculate_taxes_and_totals(object):
shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule) shipping_rule = frappe.get_doc("Shipping Rule", self.doc.shipping_rule)
shipping_rule.apply(self.doc) shipping_rule.apply(self.doc)
self._calculate()
def calculate_taxes(self): def calculate_taxes(self):
rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment') rounding_adjustment_computed = self.doc.get('is_consolidated') and self.doc.get('rounding_adjustment')
if not rounding_adjustment_computed: if not rounding_adjustment_computed:

View File

@ -214,6 +214,7 @@ class Lead(SellingController):
}) })
contact.insert(ignore_permissions=True) contact.insert(ignore_permissions=True)
contact.reload() # load changes by hooks on contact
return contact return contact

View File

@ -48,7 +48,6 @@ def get_product_filter_data(query_args=None):
sub_categories = [] sub_categories = []
if item_group: if item_group:
field_filters['item_group'] = item_group
sub_categories = get_child_groups_for_website(item_group, immediate=True) sub_categories = get_child_groups_for_website(item_group, immediate=True)
engine = ProductQuery() engine = ProductQuery()

View File

@ -14,6 +14,8 @@ class ProductFiltersBuilder:
self.item_group = item_group self.item_group = item_group
def get_field_filters(self): def get_field_filters(self):
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
if not self.item_group and not self.doc.enable_field_filters: if not self.item_group and not self.doc.enable_field_filters:
return return
@ -25,18 +27,26 @@ class ProductFiltersBuilder:
fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)] fields = [item_meta.get_field(field) for field in filter_fields if item_meta.has_field(field)]
for df in fields: for df in fields:
item_filters, item_or_filters = {}, [] item_filters, item_or_filters = {"published_in_website": 1}, []
link_doctype_values = self.get_filtered_link_doctype_records(df) link_doctype_values = self.get_filtered_link_doctype_records(df)
if df.fieldtype == "Link": if df.fieldtype == "Link":
if self.item_group: if self.item_group:
item_or_filters.extend([ include_child = frappe.db.get_value("Item Group", self.item_group, "include_descendants")
["item_group", "=", self.item_group], if include_child:
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups include_groups = get_child_groups_for_website(self.item_group, include_self=True)
]) include_groups = [x.name for x in include_groups]
item_or_filters.extend([
["item_group", "in", include_groups],
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
])
else:
item_or_filters.extend([
["item_group", "=", self.item_group],
["Website Item Group", "item_group", "=", self.item_group] # consider website item groups
])
# Get link field values attached to published items # Get link field values attached to published items
item_filters['published_in_website'] = 1
item_values = frappe.get_all( item_values = frappe.get_all(
"Item", "Item",
fields=[df.fieldname], fields=[df.fieldname],

View File

@ -46,10 +46,10 @@ class ProductQuery:
self.filter_with_discount = bool(fields.get("discount")) self.filter_with_discount = bool(fields.get("discount"))
result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0
website_item_groups = self.get_website_item_group_results(item_group, website_item_groups)
if fields: if fields:
self.build_fields_filters(fields) self.build_fields_filters(fields)
if item_group:
self.build_item_group_filters(item_group)
if search_term: if search_term:
self.build_search_filters(search_term) self.build_search_filters(search_term)
if self.settings.hide_variants: if self.settings.hide_variants:
@ -61,8 +61,6 @@ class ProductQuery:
else: else:
result, count = self.query_items(start=start) result, count = self.query_items(start=start)
result = self.combine_web_item_group_results(item_group, result, website_item_groups)
# sort combined results by ranking # sort combined results by ranking
result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) result = sorted(result, key=lambda x: x.get("ranking"), reverse=True)
@ -167,6 +165,25 @@ class ProductQuery:
# `=` will be faster than `IN` for most cases # `=` will be faster than `IN` for most cases
self.filters.append([field, "=", values]) self.filters.append([field, "=", values])
def build_item_group_filters(self, item_group):
"Add filters for Item group page and include Website Item Groups."
from erpnext.setup.doctype.item_group.item_group import get_child_groups_for_website
item_group_filters = []
item_group_filters.append(["Website Item", "item_group", "=", item_group])
# Consider Website Item Groups
item_group_filters.append(["Website Item Group", "item_group", "=", item_group])
if frappe.db.get_value("Item Group", item_group, "include_descendants"):
# include child item group's items as well
# eg. Group Node A, will show items of child 1 and child 2 as well
# on it's web page
include_groups = get_child_groups_for_website(item_group, include_self=True)
include_groups = [x.name for x in include_groups]
item_group_filters.append(["Website Item", "item_group", "in", include_groups])
self.or_filters.extend(item_group_filters)
def build_search_filters(self, search_term): def build_search_filters(self, search_term):
"""Query search term in specified fields """Query search term in specified fields
@ -190,19 +207,6 @@ class ProductQuery:
for field in search_fields: for field in search_fields:
self.or_filters.append([field, "like", search]) self.or_filters.append([field, "like", search])
def get_website_item_group_results(self, item_group, website_item_groups):
"""Get Web Items for Item Group Page via Website Item Groups."""
if item_group:
website_item_groups = frappe.db.get_all(
"Website Item",
fields=self.fields + ["`tabWebsite Item Group`.parent as wig_parent"],
filters=[
["Website Item Group", "item_group", "=", item_group],
["published", "=", 1]
]
)
return website_item_groups
def add_display_details(self, result, discount_list, cart_items): def add_display_details(self, result, discount_list, cart_items):
"""Add price and availability details in result.""" """Add price and availability details in result."""
for item in result: for item in result:
@ -278,16 +282,6 @@ class ProductQuery:
return [] return []
def combine_web_item_group_results(self, item_group, result, website_item_groups):
"""Combine results with context of website item groups into item results."""
if item_group and website_item_groups:
items_list = {row.name for row in result}
for row in website_item_groups:
if row.wig_parent not in items_list:
result.append(row)
return result
def filter_results_by_discount(self, fields, result): def filter_results_by_discount(self, fields, result):
if fields and fields.get("discount"): if fields and fields.get("discount"):
discount_percent = frappe.utils.flt(fields["discount"][0]) discount_percent = frappe.utils.flt(fields["discount"][0])

View File

@ -13,8 +13,7 @@ test_dependencies = ["Item", "Item Group"]
class TestItemGroupProductDataEngine(unittest.TestCase): class TestItemGroupProductDataEngine(unittest.TestCase):
"Test Products & Sub-Category Querying for Product Listing on Item Group Page." "Test Products & Sub-Category Querying for Product Listing on Item Group Page."
@classmethod def setUp(self):
def setUpClass(cls):
item_codes = [ item_codes = [
("Test Mobile A", "_Test Item Group B"), ("Test Mobile A", "_Test Item Group B"),
("Test Mobile B", "_Test Item Group B"), ("Test Mobile B", "_Test Item Group B"),
@ -28,8 +27,10 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
if not frappe.db.exists("Website Item", {"item_code": item_code}): if not frappe.db.exists("Website Item", {"item_code": item_code}):
create_regular_web_item(item_code, item_args=item_args) create_regular_web_item(item_code, item_args=item_args)
@classmethod frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
def tearDownClass(cls): frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 1)
def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
def test_product_listing_in_item_group(self): def test_product_listing_in_item_group(self):
@ -87,7 +88,6 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
def test_item_group_with_sub_groups(self): def test_item_group_with_sub_groups(self):
"Test Valid Sub Item Groups in Item Group Page." "Test Valid Sub Item Groups in Item Group Page."
frappe.db.set_value("Item Group", "_Test Item Group B - 1", "show_in_website", 1)
frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0) frappe.db.set_value("Item Group", "_Test Item Group B - 2", "show_in_website", 0)
result = get_product_filter_data(query_args={ result = get_product_filter_data(query_args={
@ -114,4 +114,45 @@ class TestItemGroupProductDataEngine(unittest.TestCase):
# check if child group is fetched if shown in website # check if child group is fetched if shown in website
self.assertIn("_Test Item Group B - 1", child_groups) self.assertIn("_Test Item Group B - 1", child_groups)
self.assertIn("_Test Item Group B - 2", child_groups) self.assertIn("_Test Item Group B - 2", child_groups)
def test_item_group_page_with_descendants_included(self):
"""
Test if 'include_descendants' pulls Items belonging to descendant Item Groups (Level 2 & 3).
> _Test Item Group B [Level 1]
> _Test Item Group B - 1 [Level 2]
> _Test Item Group B - 1 - 1 [Level 3]
"""
frappe.get_doc({ # create Level 3 nested child group
"doctype": "Item Group",
"is_group": 1,
"item_group_name": "_Test Item Group B - 1 - 1",
"parent_item_group": "_Test Item Group B - 1"
}).insert()
create_regular_web_item( # create an item belonging to level 3 item group
"Test Mobile F",
item_args={"item_group": "_Test Item Group B - 1 - 1"}
)
frappe.db.set_value("Item Group", "_Test Item Group B - 1 - 1", "show_in_website", 1)
# enable 'include descendants' in Level 1
frappe.db.set_value("Item Group", "_Test Item Group B", "include_descendants", 1)
result = get_product_filter_data(query_args={
"field_filters": {},
"attribute_filters": {},
"start": 0,
"item_group": "_Test Item Group B"
})
items = result.get("items")
item_codes = [item.get("item_code") for item in items]
# check if all sub groups' items are pulled
self.assertEqual(len(items), 6)
self.assertIn("Test Mobile A", item_codes)
self.assertIn("Test Mobile C", item_codes)
self.assertIn("Test Mobile E", item_codes)
self.assertIn("Test Mobile F", item_codes)

View File

@ -120,7 +120,11 @@ def place_order():
def request_for_quotation(): def request_for_quotation():
quotation = _get_cart_quotation() quotation = _get_cart_quotation()
quotation.flags.ignore_permissions = True quotation.flags.ignore_permissions = True
quotation.submit()
if get_shopping_cart_settings().save_quotations_as_draft:
quotation.save()
else:
quotation.submit()
return quotation.name return quotation.name
@frappe.whitelist() @frappe.whitelist()

View File

@ -6,7 +6,7 @@ import unittest
import frappe import frappe
from frappe.tests.utils import change_settings from frappe.tests.utils import change_settings
from frappe.utils import add_months, nowdate from frappe.utils import add_months, cint, nowdate
from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
@ -14,22 +14,17 @@ from erpnext.e_commerce.shopping_cart.cart import (
_get_cart_quotation, _get_cart_quotation,
get_cart_quotation, get_cart_quotation,
get_party, get_party,
request_for_quotation,
update_cart, update_cart,
) )
from erpnext.tests.utils import create_test_contact_and_address from erpnext.tests.utils import create_test_contact_and_address
# test_dependencies = ['Payment Terms Template']
class TestShoppingCart(unittest.TestCase): class TestShoppingCart(unittest.TestCase):
""" """
Note: Note:
Shopping Cart == Quotation Shopping Cart == Quotation
""" """
@classmethod
def tearDownClass(cls):
frappe.db.sql("delete from `tabTax Rule`")
def setUp(self): def setUp(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
create_test_contact_and_address() create_test_contact_and_address()
@ -45,6 +40,10 @@ class TestShoppingCart(unittest.TestCase):
frappe.set_user("Administrator") frappe.set_user("Administrator")
self.disable_shopping_cart() self.disable_shopping_cart()
@classmethod
def tearDownClass(cls):
frappe.db.sql("delete from `tabTax Rule`")
def test_get_cart_new_user(self): def test_get_cart_new_user(self):
self.login_as_new_user() self.login_as_new_user()
@ -179,6 +178,28 @@ class TestShoppingCart(unittest.TestCase):
# test if items are rendered without error # test if items are rendered without error
frappe.render_template("templates/includes/cart/cart_items.html", cart) frappe.render_template("templates/includes/cart/cart_items.html", cart)
@change_settings("E Commerce Settings",{
"save_quotations_as_draft": 1
})
def test_cart_without_checkout_and_draft_quotation(self):
"Test impact of 'save_quotations_as_draft' checkbox."
frappe.local.shopping_cart_settings = None
# add item to cart
update_cart("_Test Item", 1)
quote_name = request_for_quotation() # Request for Quote
quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
self.assertEqual(quote_doctstatus, 0)
frappe.db.set_value("E Commerce Settings", None, "save_quotations_as_draft", 0)
frappe.local.shopping_cart_settings = None
update_cart("_Test Item", 1)
quote_name = request_for_quotation() # Request for Quote
quote_doctstatus = cint(frappe.db.get_value("Quotation", quote_name, "docstatus"))
self.assertEqual(quote_doctstatus, 1)
def create_tax_rule(self): def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0] tax_rule = frappe.get_test_records("Tax Rule")[0]
try: try:

View File

@ -25,7 +25,9 @@ class TestAttendance(FrappeTestCase):
self.assertEqual(attendance, fetch_attendance) self.assertEqual(attendance, fetch_attendance)
def test_unmarked_days(self): def test_unmarked_days(self):
first_day = get_first_day(getdate()) now = now_datetime()
previous_month = now.month - 1
first_day = now.replace(day=1).replace(month=previous_month).date()
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee}) frappe.db.delete('Attendance', {'employee': employee})
@ -34,7 +36,7 @@ class TestAttendance(FrappeTestCase):
holiday_list = make_holiday_list() holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
first_sunday = get_first_sunday(holiday_list) first_sunday = get_first_sunday(holiday_list, for_date=first_day)
mark_attendance(employee, first_day, 'Present') mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day) month_name = get_month_name(first_day)
@ -49,7 +51,9 @@ class TestAttendance(FrappeTestCase):
self.assertIn(first_sunday, unmarked_days) self.assertIn(first_sunday, unmarked_days)
def test_unmarked_days_excluding_holidays(self): def test_unmarked_days_excluding_holidays(self):
first_day = get_first_day(getdate()) now = now_datetime()
previous_month = now.month - 1
first_day = now.replace(day=1).replace(month=previous_month).date()
employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1)) employee = make_employee('test_unmarked_days@example.com', date_of_joining=add_days(first_day, -1))
frappe.db.delete('Attendance', {'employee': employee}) frappe.db.delete('Attendance', {'employee': employee})
@ -58,7 +62,7 @@ class TestAttendance(FrappeTestCase):
holiday_list = make_holiday_list() holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list) frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
first_sunday = get_first_sunday(holiday_list) first_sunday = get_first_sunday(holiday_list, for_date=first_day)
mark_attendance(employee, first_day, 'Present') mark_attendance(employee, first_day, 'Present')
month_name = get_month_name(first_day) month_name = get_month_name(first_day)
@ -83,6 +87,10 @@ class TestAttendance(FrappeTestCase):
relieving_date=relieving_date) relieving_date=relieving_date)
frappe.db.delete('Attendance', {'employee': employee}) frappe.db.delete('Attendance', {'employee': employee})
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
holiday_list = make_holiday_list()
frappe.db.set_value('Employee', employee, 'holiday_list', holiday_list)
attendance_date = add_days(first_day, 2) attendance_date = add_days(first_day, 2)
mark_attendance(employee, attendance_date, 'Present') mark_attendance(employee, attendance_date, 'Present')
month_name = get_month_name(first_day) month_name = get_month_name(first_day)

View File

@ -495,7 +495,6 @@ def get_number_of_leave_days(employee, leave_type, from_date, to_date, half_day
number_of_days = date_diff(to_date, from_date) + .5 number_of_days = date_diff(to_date, from_date) + .5
else: else:
number_of_days = date_diff(to_date, from_date) + 1 number_of_days = date_diff(to_date, from_date) + 1
else: else:
number_of_days = date_diff(to_date, from_date) + 1 number_of_days = date_diff(to_date, from_date) + 1

View File

@ -133,7 +133,9 @@ class TestLeaveApplication(unittest.TestCase):
holiday_list = make_holiday_list() holiday_list = make_holiday_list()
employee = get_employee() employee = get_employee()
frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) original_holiday_list = employee.holiday_list
frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list) first_sunday = get_first_sunday(holiday_list)
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name)
@ -143,6 +145,8 @@ class TestLeaveApplication(unittest.TestCase):
leave_application.cancel() leave_application.cancel()
frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
def test_attendance_update_for_exclude_holidays(self): def test_attendance_update_for_exclude_holidays(self):
# Case 2: leave type with 'Include holidays within leaves as leaves' disabled # Case 2: leave type with 'Include holidays within leaves as leaves' disabled
frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1) frappe.delete_doc_if_exists("Leave Type", "Test Do Not Include Holidays", force=1)
@ -157,7 +161,8 @@ class TestLeaveApplication(unittest.TestCase):
holiday_list = make_holiday_list() holiday_list = make_holiday_list()
employee = get_employee() employee = get_employee()
frappe.db.set_value("Company", employee.company, "default_holiday_list", holiday_list) original_holiday_list = employee.holiday_list
frappe.db.set_value("Employee", employee.name, "holiday_list", holiday_list)
first_sunday = get_first_sunday(holiday_list) first_sunday = get_first_sunday(holiday_list)
# already marked attendance on a holiday should be deleted in this case # already marked attendance on a holiday should be deleted in this case
@ -177,7 +182,7 @@ class TestLeaveApplication(unittest.TestCase):
attendance.flags.ignore_validate = True attendance.flags.ignore_validate = True
attendance.save() attendance.save()
leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name) leave_application = make_leave_application(employee.name, first_sunday, add_days(first_sunday, 3), leave_type.name, employee.company)
leave_application.reload() leave_application.reload()
# holiday should be excluded while marking attendance # holiday should be excluded while marking attendance
self.assertEqual(leave_application.total_leave_days, 3) self.assertEqual(leave_application.total_leave_days, 3)
@ -189,6 +194,8 @@ class TestLeaveApplication(unittest.TestCase):
# attendance on non-holiday updated # attendance on non-holiday updated
self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave") self.assertEqual(frappe.db.get_value("Attendance", attendance.name, "status"), "On Leave")
frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
def test_block_list(self): def test_block_list(self):
self._clear_roles() self._clear_roles()
@ -327,7 +334,8 @@ class TestLeaveApplication(unittest.TestCase):
employee = get_employee() employee = get_employee()
default_holiday_list = make_holiday_list() default_holiday_list = make_holiday_list()
frappe.db.set_value("Company", employee.company, "default_holiday_list", default_holiday_list) original_holiday_list = employee.holiday_list
frappe.db.set_value("Employee", employee.name, "holiday_list", default_holiday_list)
first_sunday = get_first_sunday(default_holiday_list) first_sunday = get_first_sunday(default_holiday_list)
optional_leave_date = add_days(first_sunday, 1) optional_leave_date = add_days(first_sunday, 1)
@ -378,6 +386,8 @@ class TestLeaveApplication(unittest.TestCase):
# check leave balance is reduced # check leave balance is reduced
self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9) self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9)
frappe.db.set_value("Employee", employee.name, "holiday_list", original_holiday_list)
def test_leaves_allowed(self): def test_leaves_allowed(self):
employee = get_employee() employee = get_employee()
leave_period = get_leave_period() leave_period = get_leave_period()
@ -782,9 +792,10 @@ def allocate_leaves(employee, leave_period, leave_type, new_leaves_allocated, el
allocate_leave.submit() allocate_leave.submit()
def get_first_sunday(holiday_list): def get_first_sunday(holiday_list, for_date=None):
month_start_date = get_first_day(nowdate()) date = for_date or getdate()
month_end_date = get_last_day(nowdate()) month_start_date = get_first_day(date)
month_end_date = get_last_day(date)
first_sunday = frappe.db.sql(""" first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday` select holiday_date from `tabHoliday`
where parent = %s where parent = %s

View File

@ -0,0 +1,44 @@
import frappe
from dateutil.relativedelta import relativedelta
from frappe.tests.utils import FrappeTestCase
from frappe.utils import now_datetime
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.report.monthly_attendance_sheet.monthly_attendance_sheet import execute
class TestMonthlyAttendanceSheet(FrappeTestCase):
def setUp(self):
self.employee = make_employee("test_employee@example.com")
frappe.db.delete('Attendance', {'employee': self.employee})
def test_monthly_attendance_sheet_report(self):
now = now_datetime()
previous_month = now.month - 1
previous_month_first = now.replace(day=1).replace(month=previous_month).date()
company = frappe.db.get_value('Employee', self.employee, 'company')
# mark different attendance status on first 3 days of previous month
mark_attendance(self.employee, previous_month_first, 'Absent')
mark_attendance(self.employee, previous_month_first + relativedelta(days=1), 'Present')
mark_attendance(self.employee, previous_month_first + relativedelta(days=2), 'On Leave')
filters = frappe._dict({
'month': previous_month,
'year': now.year,
'company': company,
})
report = execute(filters=filters)
employees = report[1][0]
datasets = report[3]['data']['datasets']
absent = datasets[0]['values']
present = datasets[1]['values']
leaves = datasets[2]['values']
# ensure correct attendance is reflect on the report
self.assertIn(self.employee, employees)
self.assertEqual(absent[0], 1)
self.assertEqual(present[1], 1)
self.assertEqual(leaves[2], 1)

View File

@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import functools import functools
import re
from collections import deque from collections import deque
from operator import itemgetter from operator import itemgetter
from typing import List from typing import List
@ -103,25 +104,33 @@ class BOM(WebsiteGenerator):
) )
def autoname(self): def autoname(self):
names = frappe.db.sql_list("""select name from `tabBOM` where item=%s""", self.item) # ignore amended documents while calculating current index
existing_boms = frappe.get_all(
"BOM",
filters={"item": self.item, "amended_from": ["is", "not set"]},
pluck="name"
)
if names: if existing_boms:
# name can be BOM/ITEM/001, BOM/ITEM/001-1, BOM-ITEM-001, BOM-ITEM-001-1 index = self.get_next_version_index(existing_boms)
# split by item
names = [name.split(self.item, 1) for name in names]
names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)]
# split by (-) if cancelled
if names:
names = [cint(name.split('-')[-1]) for name in names]
idx = max(names) + 1
else:
idx = 1
else: else:
idx = 1 index = 1
prefix = self.doctype
suffix = "%.3i" % index # convert index to string (1 -> "001")
bom_name = f"{prefix}-{self.item}-{suffix}"
if len(bom_name) <= 140:
name = bom_name
else:
# since max characters for name is 140, remove enough characters from the
# item name to fit the prefix, suffix and the separators
truncated_length = 140 - (len(prefix) + len(suffix) + 2)
truncated_item_name = self.item[:truncated_length]
# if a partial word is found after truncate, remove the extra characters
truncated_item_name = truncated_item_name.rsplit(" ", 1)[0]
name = f"{prefix}-{truncated_item_name}-{suffix}"
name = 'BOM-' + self.item + ('-%.3i' % idx)
if frappe.db.exists("BOM", name): if frappe.db.exists("BOM", name):
conflicting_bom = frappe.get_doc("BOM", name) conflicting_bom = frappe.get_doc("BOM", name)
@ -134,6 +143,26 @@ class BOM(WebsiteGenerator):
self.name = name self.name = name
@staticmethod
def get_next_version_index(existing_boms: List[str]) -> int:
# split by "/" and "-"
delimiters = ["/", "-"]
pattern = "|".join(map(re.escape, delimiters))
bom_parts = [re.split(pattern, bom_name) for bom_name in existing_boms]
# filter out BOMs that do not follow the following formats: BOM/ITEM/001, BOM-ITEM-001
valid_bom_parts = list(filter(lambda x: len(x) > 1 and x[-1], bom_parts))
# extract the current index from the BOM parts
if valid_bom_parts:
# handle cancelled and submitted documents
indexes = [cint(part[-1]) for part in valid_bom_parts]
index = max(indexes) + 1
else:
index = 1
return index
def validate(self): def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-') self.route = frappe.scrub(self.name).replace('_', '-')
@ -192,12 +221,13 @@ class BOM(WebsiteGenerator):
if self.routing: if self.routing:
self.set("operations", []) self.set("operations", [])
fields = ["sequence_id", "operation", "workstation", "description", fields = ["sequence_id", "operation", "workstation", "description",
"time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate"] "time_in_mins", "batch_size", "operating_cost", "idx", "hour_rate",
"set_cost_based_on_bom_qty", "fixed_time"]
for row in frappe.get_all("BOM Operation", fields = fields, for row in frappe.get_all("BOM Operation", fields = fields,
filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"): filters = {'parenttype': 'Routing', 'parent': self.routing}, order_by="sequence_id, idx"):
child = self.append('operations', row) child = self.append('operations', row)
child.hour_rate = flt(row.hour_rate / self.conversion_rate, 2) child.hour_rate = flt(row.hour_rate / self.conversion_rate, child.precision("hour_rate"))
def set_bom_material_details(self): def set_bom_material_details(self):
for item in self.get("items"): for item in self.get("items"):

View File

@ -432,6 +432,69 @@ class TestBOM(FrappeTestCase):
self.assertEqual(bom.transfer_material_against, "Work Order") self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete() bom.delete()
def test_bom_name_length(self):
""" test >140 char names"""
bom_tree = {
"x" * 140 : {
" ".join(["abc"] * 35): {}
}
}
create_nested_bom(bom_tree, prefix="")
def test_version_index(self):
bom = frappe.new_doc("BOM")
version_index_test_cases = [
(1, []),
(1, ["BOM#XYZ"]),
(2, ["BOM/ITEM/001"]),
(2, ["BOM-ITEM-001"]),
(3, ["BOM-ITEM-001", "BOM-ITEM-002"]),
(4, ["BOM-ITEM-001", "BOM-ITEM-002", "BOM-ITEM-003"]),
]
for expected_index, existing_boms in version_index_test_cases:
with self.subTest():
self.assertEqual(expected_index, bom.get_next_version_index(existing_boms),
msg=f"Incorrect index for {existing_boms}")
def test_bom_versioning(self):
bom_tree = {
frappe.generate_hash(length=10) : {
frappe.generate_hash(length=10): {}
}
}
bom = create_nested_bom(bom_tree, prefix="")
self.assertEqual(int(bom.name.split("-")[-1]), 1)
original_bom_name = bom.name
bom.cancel()
bom.reload()
self.assertEqual(bom.name, original_bom_name)
# create a new amendment
amendment = frappe.copy_doc(bom)
amendment.docstatus = 0
amendment.amended_from = bom.name
amendment.save()
amendment.submit()
amendment.reload()
self.assertNotEqual(amendment.name, bom.name)
# `origname-001-1` version
self.assertEqual(int(amendment.name.split("-")[-1]), 1)
self.assertEqual(int(amendment.name.split("-")[-2]), 1)
# create a new version
version = frappe.copy_doc(amendment)
version.docstatus = 0
version.amended_from = None
version.save()
self.assertNotEqual(amendment.name, version.name)
self.assertEqual(int(version.name.split("-")[-1]), 2)
def get_default_bom(item_code="_Test FG Item 2"): def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1}) return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@ -66,7 +66,8 @@
"label": "Hour Rate", "label": "Hour Rate",
"oldfieldname": "hour_rate", "oldfieldname": "hour_rate",
"oldfieldtype": "Currency", "oldfieldtype": "Currency",
"options": "currency" "options": "currency",
"precision": "2"
}, },
{ {
"description": "In minutes", "description": "In minutes",
@ -186,7 +187,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-12-15 03:00:00.473173", "modified": "2022-03-10 06:19:08.462027",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM Operation", "name": "BOM Operation",

View File

@ -5,10 +5,13 @@ def execute():
frappe.reload_doc('accounts', 'doctype', 'bank', force=1) frappe.reload_doc('accounts', 'doctype', 'bank', force=1)
if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'): if frappe.db.table_exists('Bank') and frappe.db.table_exists('Bank Account') and frappe.db.has_column('Bank Account', 'swift_number'):
frappe.db.sql(""" try:
UPDATE `tabBank` b, `tabBank Account` ba frappe.db.sql("""
SET b.swift_number = ba.swift_number WHERE b.name = ba.bank UPDATE `tabBank` b, `tabBank Account` ba
""") SET b.swift_number = ba.swift_number WHERE b.name = ba.bank
""")
except Exception as e:
frappe.log_error(e, title="Patch Migration Failed")
frappe.reload_doc('accounts', 'doctype', 'bank_account') frappe.reload_doc('accounts', 'doctype', 'bank_account')
frappe.reload_doc('accounts', 'doctype', 'payment_request') frappe.reload_doc('accounts', 'doctype', 'payment_request')

View File

@ -1,14 +1,12 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe import _ from frappe import _
from frappe.utils import flt from frappe.utils import flt
def execute(filters=None): def execute(filters=None):
columns, data = [], []
data = get_data(filters) data = get_data(filters)
columns = get_columns() columns = get_columns()
charts = get_chart_data(data) charts = get_chart_data(data)

View File

@ -39,6 +39,8 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this._calculate_taxes_and_totals(); this._calculate_taxes_and_totals();
this.calculate_discount_amount(); this.calculate_discount_amount();
this.calculate_shipping_charges();
// Advance calculation applicable to Sales /Purchase Invoice // Advance calculation applicable to Sales /Purchase Invoice
if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype) if(in_list(["Sales Invoice", "POS Invoice", "Purchase Invoice"], this.frm.doc.doctype)
&& this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) { && this.frm.doc.docstatus < 2 && !this.frm.doc.is_return) {
@ -81,7 +83,6 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
this.initialize_taxes(); this.initialize_taxes();
this.determine_exclusive_rate(); this.determine_exclusive_rate();
this.calculate_net_total(); this.calculate_net_total();
this.calculate_shipping_charges();
this.calculate_taxes(); this.calculate_taxes();
this.manipulate_grand_total_for_inclusive_tax(); this.manipulate_grand_total_for_inclusive_tax();
this.calculate_totals(); this.calculate_totals();
@ -275,6 +276,7 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]); frappe.model.round_floats_in(this.frm.doc, ["total", "base_total", "net_total", "base_net_total"]);
if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) { if (frappe.meta.get_docfield(this.frm.doc.doctype, "shipping_rule", this.frm.doc.name)) {
this.shipping_rule(); this.shipping_rule();
this._calculate_taxes_and_totals();
} }
} }

View File

@ -343,8 +343,7 @@ def run_query(filters, extra_fields, extra_joins, extra_filters, as_dict=1):
/* against number or, if empty, party against number */ /* against number or, if empty, party against number */
%(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)', %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
/* disable automatic VAT deduction */ '' as 'BU-Schlüssel',
'40' as 'BU-Schlüssel',
gl.posting_date as 'Belegdatum', gl.posting_date as 'Belegdatum',
gl.voucher_no as 'Belegfeld 1', gl.voucher_no as 'Belegfeld 1',

View File

@ -30,7 +30,6 @@ def execute(filters=None):
if region != 'United States': if region != 'United States':
return [], [] return [], []
data = []
columns = get_columns() columns = get_columns()
conditions = "" conditions = ""
if filters.supplier_group: if filters.supplier_group:

View File

@ -118,8 +118,7 @@ def make_customer():
"customer_type": "Company", "customer_type": "Company",
}) })
customer.insert() customer.insert()
else:
customer = frappe.get_doc("Customer", "_Test UAE Customer")
def make_supplier(): def make_supplier():
if not frappe.db.exists("Supplier", "_Test UAE Supplier"): if not frappe.db.exists("Supplier", "_Test UAE Supplier"):

View File

@ -1477,6 +1477,28 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(so.items[0].work_order_qty, wo.produced_qty) self.assertEqual(so.items[0].work_order_qty, wo.produced_qty)
self.assertEqual(mr.status, "Manufactured") self.assertEqual(mr.status, "Manufactured")
def test_sales_order_with_shipping_rule(self):
from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule
shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test")
sales_order = make_sales_order(do_not_save=True)
sales_order.shipping_rule = shipping_rule.name
sales_order.items[0].qty = 1
sales_order.save()
self.assertEqual(sales_order.taxes[0].tax_amount, 50)
sales_order.items[0].qty = 2
sales_order.save()
self.assertEqual(sales_order.taxes[0].tax_amount, 100)
sales_order.items[0].qty = 3
sales_order.save()
self.assertEqual(sales_order.taxes[0].tax_amount, 200)
sales_order.items[0].qty = 21
sales_order.save()
self.assertEqual(sales_order.taxes[0].tax_amount, 0)
def automatically_fetch_payment_terms(enable=1): def automatically_fetch_payment_terms(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.automatically_fetch_payment_terms = enable accounts_settings.automatically_fetch_payment_terms = enable

View File

@ -20,12 +20,14 @@
"sec_break_taxes", "sec_break_taxes",
"taxes", "taxes",
"sb9", "sb9",
"show_in_website",
"route", "route",
"weightage",
"slideshow",
"website_title", "website_title",
"description", "description",
"show_in_website",
"include_descendants",
"column_break_16",
"weightage",
"slideshow",
"website_specifications", "website_specifications",
"website_filters_section", "website_filters_section",
"filter_fields", "filter_fields",
@ -111,7 +113,7 @@
}, },
{ {
"default": "0", "default": "0",
"description": "Check this if you want to show in website", "description": "Make Item Group visible in website",
"fieldname": "show_in_website", "fieldname": "show_in_website",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Show in Website" "label": "Show in Website"
@ -124,6 +126,7 @@
"unique": 1 "unique": 1
}, },
{ {
"depends_on": "show_in_website",
"fieldname": "weightage", "fieldname": "weightage",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Weightage" "label": "Weightage"
@ -186,6 +189,8 @@
"report_hide": 1 "report_hide": 1
}, },
{ {
"collapsible": 1,
"depends_on": "show_in_website",
"fieldname": "website_filters_section", "fieldname": "website_filters_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Website Filters" "label": "Website Filters"
@ -203,9 +208,22 @@
"options": "Website Attribute" "options": "Website Attribute"
}, },
{ {
"depends_on": "show_in_website",
"fieldname": "website_title", "fieldname": "website_title",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Title" "label": "Title"
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "show_in_website",
"description": "Include Website Items belonging to child Item Groups",
"fieldname": "include_descendants",
"fieldtype": "Check",
"label": "Include Descendants"
} }
], ],
"icon": "fa fa-sitemap", "icon": "fa fa-sitemap",
@ -214,11 +232,12 @@
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"max_attachments": 3, "max_attachments": 3,
"modified": "2021-02-18 13:40:30.049650", "modified": "2022-03-09 12:27:11.055782",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Item Group", "name": "Item Group",
"name_case": "Title Case", "name_case": "Title Case",
"naming_rule": "By fieldname",
"nsm_parent_field": "parent_item_group", "nsm_parent_field": "parent_item_group",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
@ -285,5 +304,6 @@
"search_fields": "parent_item_group", "search_fields": "parent_item_group",
"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": []
} }

View File

@ -111,7 +111,7 @@ class ItemGroup(NestedSet, WebsiteGenerator):
from erpnext.stock.doctype.item.item import validate_item_default_company_links from erpnext.stock.doctype.item.item import validate_item_default_company_links
validate_item_default_company_links(self.item_group_defaults) validate_item_default_company_links(self.item_group_defaults)
def get_child_groups_for_website(item_group_name, immediate=False): def get_child_groups_for_website(item_group_name, immediate=False, include_self=False):
"""Returns child item groups *excluding* passed group.""" """Returns child item groups *excluding* passed group."""
item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1) item_group = frappe.get_cached_value("Item Group", item_group_name, ["lft", "rgt"], as_dict=1)
filters = { filters = {
@ -123,6 +123,12 @@ def get_child_groups_for_website(item_group_name, immediate=False):
if immediate: if immediate:
filters["parent_item_group"] = item_group_name filters["parent_item_group"] = item_group_name
if include_self:
filters.update({
"lft": [">=", item_group.lft],
"rgt": ["<=", item_group.rgt]
})
return frappe.get_all( return frappe.get_all(
"Item Group", "Item Group",
filters=filters, filters=filters,

View File

@ -342,25 +342,21 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True):
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
# Billed against Sales Order directly # Billed against Sales Order directly
si = frappe.qb.DocType("Sales Invoice").as_("si")
si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item") si_item = frappe.qb.DocType("Sales Invoice Item").as_("si_item")
sum_amount = Sum(si_item.amount).as_("amount") sum_amount = Sum(si_item.amount).as_("amount")
billed_against_so = frappe.qb.from_(si).from_(si_item).select(sum_amount).where( billed_against_so = frappe.qb.from_(si_item).select(sum_amount).where(
(si_item.parent == si.name) &
(si_item.so_detail == so_detail) & (si_item.so_detail == so_detail) &
((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) & ((si_item.dn_detail.isnull()) | (si_item.dn_detail == '')) &
(si_item.docstatus == 1) & (si_item.docstatus == 1)
(si.update_stock == 0)
).run() ).run()
billed_against_so = billed_against_so and billed_against_so[0][0] or 0 billed_against_so = billed_against_so and billed_against_so[0][0] or 0
# Get all Delivery Note Item rows against the Sales Order Item row # Get all Delivery Note Item rows against the Sales Order Item row
dn = frappe.qb.DocType("Delivery Note").as_("dn") dn = frappe.qb.DocType("Delivery Note").as_("dn")
dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item") dn_item = frappe.qb.DocType("Delivery Note Item").as_("dn_item")
dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent, dn_item.stock_qty, dn_item.returned_qty).where( dn_details = frappe.qb.from_(dn).from_(dn_item).select(dn_item.name, dn_item.amount, dn_item.si_detail, dn_item.parent).where(
(dn.name == dn_item.parent) & (dn.name == dn_item.parent) &
(dn_item.so_detail == so_detail) & (dn_item.so_detail == so_detail) &
(dn.docstatus == 1) & (dn.docstatus == 1) &
@ -385,11 +381,7 @@ def update_billed_amount_based_on_so(so_detail, update_modified=True):
# Distribute billed amount directly against SO between DNs based on FIFO # Distribute billed amount directly against SO between DNs based on FIFO
if billed_against_so and billed_amt_agianst_dn < dnd.amount: if billed_against_so and billed_amt_agianst_dn < dnd.amount:
if dnd.returned_qty: pending_to_bill = flt(dnd.amount) - billed_amt_agianst_dn
pending_to_bill = flt(dnd.amount) * (dnd.stock_qty - dnd.returned_qty) / dnd.stock_qty
else:
pending_to_bill = flt(dnd.amount)
pending_to_bill -= billed_amt_agianst_dn
if pending_to_bill <= billed_against_so: if pending_to_bill <= billed_against_so:
billed_amt_agianst_dn += pending_to_bill billed_amt_agianst_dn += pending_to_bill
billed_against_so -= pending_to_bill billed_against_so -= pending_to_bill

View File

@ -165,21 +165,21 @@ frappe.ui.form.on("Item", {
frm.set_value('has_batch_no', 0); frm.set_value('has_batch_no', 0);
frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset);
frm.call({ frappe.call({
method: "set_asset_naming_series", method: "erpnext.stock.doctype.item.item.get_asset_naming_series",
doc: frm.doc, callback: function(r) {
callback: function() {
frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1); frm.set_value("is_stock_item", frm.doc.is_fixed_asset ? 0 : 1);
frm.trigger("set_asset_naming_series"); frm.events.set_asset_naming_series(frm, r.message);
} }
}); });
frm.trigger('auto_create_assets'); frm.trigger('auto_create_assets');
}, },
set_asset_naming_series: function(frm) { set_asset_naming_series: function(frm, asset_naming_series) {
if (frm.doc.__onload && frm.doc.__onload.asset_naming_series) { if ((frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series) {
frm.set_df_property("asset_naming_series", "options", frm.doc.__onload.asset_naming_series); let naming_series = (frm.doc.__onload && frm.doc.__onload.asset_naming_series) || asset_naming_series;
frm.set_df_property("asset_naming_series", "options", naming_series);
} }
}, },

View File

@ -50,15 +50,7 @@ class DataValidationError(frappe.ValidationError):
class Item(Document): class Item(Document):
def onload(self): def onload(self):
self.set_onload('stock_exists', self.stock_ledger_created()) self.set_onload('stock_exists', self.stock_ledger_created())
self.set_asset_naming_series() self.set_onload('asset_naming_series', get_asset_naming_series())
@frappe.whitelist()
def set_asset_naming_series(self):
if not hasattr(self, '_asset_naming_series'):
from erpnext.assets.doctype.asset.asset import get_asset_naming_series
self._asset_naming_series = get_asset_naming_series()
self.set_onload('asset_naming_series', self._asset_naming_series)
def autoname(self): def autoname(self):
if frappe.db.get_default("item_naming_by") == "Naming Series": if frappe.db.get_default("item_naming_by") == "Naming Series":
@ -400,6 +392,7 @@ class Item(Document):
self.validate_properties_before_merge(new_name) self.validate_properties_before_merge(new_name)
self.validate_duplicate_product_bundles_before_merge(old_name, new_name) self.validate_duplicate_product_bundles_before_merge(old_name, new_name)
self.validate_duplicate_website_item_before_merge(old_name, new_name) self.validate_duplicate_website_item_before_merge(old_name, new_name)
self.delete_old_bins(old_name)
def after_rename(self, old_name, new_name, merge): def after_rename(self, old_name, new_name, merge):
if merge: if merge:
@ -428,6 +421,9 @@ class Item(Document):
frappe.db.set_value(dt, d.name, "item_wise_tax_detail", frappe.db.set_value(dt, d.name, "item_wise_tax_detail",
json.dumps(item_wise_tax_detail), update_modified=False) json.dumps(item_wise_tax_detail), update_modified=False)
def delete_old_bins(self, old_name):
frappe.db.delete("Bin", {"item_code": old_name})
def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name): def validate_duplicate_item_in_stock_reconciliation(self, old_name, new_name):
records = frappe.db.sql(""" SELECT parent, COUNT(*) as records records = frappe.db.sql(""" SELECT parent, COUNT(*) as records
FROM `tabStock Reconciliation Item` FROM `tabStock Reconciliation Item`
@ -508,11 +504,11 @@ class Item(Document):
existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock") existing_allow_negative_stock = frappe.db.get_value("Stock Settings", None, "allow_negative_stock")
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
repost_stock_for_warehouses = frappe.db.sql_list("""select distinct warehouse repost_stock_for_warehouses = frappe.get_all("Stock Ledger Entry",
from tabBin where item_code=%s""", new_name) "warehouse", filters={"item_code": new_name}, pluck="warehouse", distinct=True)
# Delete all existing bins to avoid duplicate bins for the same item and warehouse # Delete all existing bins to avoid duplicate bins for the same item and warehouse
frappe.db.sql("delete from `tabBin` where item_code=%s", new_name) frappe.db.delete("Bin", {"item_code": new_name})
for warehouse in repost_stock_for_warehouses: for warehouse in repost_stock_for_warehouses:
repost_stock(new_name, warehouse) repost_stock(new_name, warehouse)
@ -999,7 +995,7 @@ def get_uom_conv_factor(uom, stock_uom):
if uom == stock_uom: if uom == stock_uom:
return 1.0 return 1.0
from_uom, to_uom = uom, stock_uom # renaming for readability from_uom, to_uom = uom, stock_uom # renaming for readability
exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1) exact_match = frappe.db.get_value("UOM Conversion Factor", {"to_uom": to_uom, "from_uom": from_uom}, ["value"], as_dict=1)
if exact_match: if exact_match:
@ -1011,9 +1007,9 @@ def get_uom_conv_factor(uom, stock_uom):
# This attempts to try and get conversion from intermediate UOM. # This attempts to try and get conversion from intermediate UOM.
# case: # case:
# g -> mg = 1000 # g -> mg = 1000
# g -> kg = 0.001 # g -> kg = 0.001
# therefore kg -> mg = 1000 / 0.001 = 1,000,000 # therefore kg -> mg = 1000 / 0.001 = 1,000,000
intermediate_match = frappe.db.sql(""" intermediate_match = frappe.db.sql("""
select (first.value / second.value) as value select (first.value / second.value) as value
from `tabUOM Conversion Factor` first from `tabUOM Conversion Factor` first
@ -1072,3 +1068,11 @@ def validate_item_default_company_links(item_defaults: List[ItemDefault]) -> Non
frappe.bold(item_default.company), frappe.bold(item_default.company),
frappe.bold(frappe.unscrub(field)) frappe.bold(frappe.unscrub(field))
), title=_("Invalid Item Defaults")) ), title=_("Invalid Item Defaults"))
@frappe.whitelist()
def get_asset_naming_series():
from erpnext.assets.doctype.asset.asset import get_asset_naming_series
return get_asset_naming_series()

View File

@ -371,23 +371,24 @@ class TestItem(FrappeTestCase):
variant.save() variant.save()
def test_item_merging(self): def test_item_merging(self):
create_item("Test Item for Merging 1") old = create_item(frappe.generate_hash(length=20)).name
create_item("Test Item for Merging 2") new = create_item(frappe.generate_hash(length=20)).name
make_stock_entry(item_code="Test Item for Merging 1", target="_Test Warehouse - _TC", make_stock_entry(item_code=old, target="_Test Warehouse - _TC",
qty=1, rate=100) qty=1, rate=100)
make_stock_entry(item_code="Test Item for Merging 2", target="_Test Warehouse 1 - _TC", make_stock_entry(item_code=old, target="_Test Warehouse 1 - _TC",
qty=1, rate=100)
make_stock_entry(item_code=new, target="_Test Warehouse 1 - _TC",
qty=1, rate=100) qty=1, rate=100)
frappe.rename_doc("Item", "Test Item for Merging 1", "Test Item for Merging 2", merge=True) frappe.rename_doc("Item", old, new, merge=True)
self.assertFalse(frappe.db.exists("Item", "Test Item for Merging 1")) self.assertFalse(frappe.db.exists("Item", old))
self.assertTrue(frappe.db.get_value("Bin", self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse - _TC"})) {"item_code": new, "warehouse": "_Test Warehouse - _TC"}))
self.assertTrue(frappe.db.get_value("Bin", self.assertTrue(frappe.db.get_value("Bin",
{"item_code": "Test Item for Merging 2", "warehouse": "_Test Warehouse 1 - _TC"})) {"item_code": new, "warehouse": "_Test Warehouse 1 - _TC"}))
def test_item_merging_with_product_bundle(self): def test_item_merging_with_product_bundle(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle

View File

@ -214,6 +214,7 @@ frappe.ui.form.on('Material Request', {
material_request_type: frm.doc.material_request_type, material_request_type: frm.doc.material_request_type,
plc_conversion_rate: 1, plc_conversion_rate: 1,
rate: item.rate, rate: item.rate,
uom: item.uom,
conversion_factor: item.conversion_factor conversion_factor: item.conversion_factor
}, },
overwrite_warehouse: overwrite_warehouse overwrite_warehouse: overwrite_warehouse
@ -392,6 +393,7 @@ frappe.ui.form.on("Material Request Item", {
item_code: function(frm, doctype, name) { item_code: function(frm, doctype, name) {
const item = locals[doctype][name]; const item = locals[doctype][name];
item.rate = 0; item.rate = 0;
item.uom = '';
set_schedule_date(frm); set_schedule_date(frm);
frm.events.get_item_data(frm, item, true); frm.events.get_item_data(frm, item, true);
}, },

View File

@ -769,8 +769,7 @@ class TestPurchaseReceipt(FrappeTestCase):
update_purchase_receipt_status, update_purchase_receipt_status,
) )
pr = make_purchase_receipt(do_not_submit=True) pr = make_purchase_receipt()
pr.submit()
update_purchase_receipt_status(pr.name, "Closed") update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual( self.assertEqual(

View File

@ -118,7 +118,8 @@ def repost(doc):
doc.set_status('Failed') doc.set_status('Failed')
raise raise
finally: finally:
frappe.db.commit() if not frappe.flags.in_test:
frappe.db.commit()
def repost_sl_entries(doc): def repost_sl_entries(doc):
if doc.based_on == 'Transaction': if doc.based_on == 'Transaction':

View File

@ -18,7 +18,6 @@ def execute(filters=None):
is_reposting_item_valuation_in_progress() is_reposting_item_valuation_in_progress()
if not filters: filters = {} if not filters: filters = {}
from_date = filters.get('from_date')
to_date = filters.get('to_date') to_date = filters.get('to_date')
if filters.get("company"): if filters.get("company"):

View File

@ -838,11 +838,13 @@ class update_entries_after(object):
for warehouse, data in self.data.items(): for warehouse, data in self.data.items():
bin_name = get_or_make_bin(self.item_code, warehouse) bin_name = get_or_make_bin(self.item_code, warehouse)
frappe.db.set_value('Bin', bin_name, { updated_values = {
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction, "actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value "stock_value": data.stock_value
}) }
if data.valuation_rate is not None:
updated_values["valuation_rate"] = data.valuation_rate
frappe.db.set_value('Bin', bin_name, updated_values)
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False): def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):

View File

@ -39,16 +39,13 @@
frappe.call(opts).then(res => { frappe.call(opts).then(res => {
let success_dialog = new frappe.ui.Dialog({ let success_dialog = new frappe.ui.Dialog({
title: __('Success'), title: __('Success'),
primary_action_label: __('View Program Content'), primary_action_label: __('OK'),
primary_action: function() { primary_action: function() {
window.location.reload(); window.location.reload();
},
secondary_action: function() {
window.location.reload();
} }
}) })
success_dialog.show(); success_dialog.show();
success_dialog.set_message(__('You have successfully enrolled for the program ')); success_dialog.set_message(__('You have successfully enrolled for the program.'));
}) })
} }
</script> </script>