Merge branch 'version-13-hotfix' into mergify/bp/version-13-hotfix/pr-29373

This commit is contained in:
Saqib Ansari
2022-01-23 18:17:56 +05:30
committed by GitHub
20 changed files with 437 additions and 133 deletions

View File

@@ -2165,9 +2165,9 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48],
["2021-06-30", 20000.0, 21311.48],
["2021-09-30", 5041.1, 26352.58]
["2020-06-30", 1366.12, 1366.12],
["2021-06-30", 20000.0, 21366.12],
["2021-09-30", 5041.1, 26407.22]
]
for i, schedule in enumerate(asset.schedules):
@@ -2215,12 +2215,12 @@ class TestSalesInvoice(unittest.TestCase):
asset.load_from_db()
expected_values = [
["2020-06-30", 1311.48, 1311.48, True],
["2021-06-30", 20000.0, 21311.48, True],
["2022-06-30", 20000.0, 41311.48, False],
["2023-06-30", 20000.0, 61311.48, False],
["2024-06-30", 20000.0, 81311.48, False],
["2025-06-06", 18688.52, 100000.0, False]
["2020-06-30", 1366.12, 1366.12, True],
["2021-06-30", 20000.0, 21366.12, True],
["2022-06-30", 20000.0, 41366.12, False],
["2023-06-30", 20000.0, 61366.12, False],
["2024-06-30", 20000.0, 81366.12, False],
["2025-06-06", 18633.88, 100000.0, False]
]
for i, schedule in enumerate(asset.schedules):

View File

@@ -200,83 +200,85 @@ class Asset(AccountsController):
if not self.available_for_use_date:
return
for d in self.get('finance_books'):
self.validate_asset_finance_books(d)
start = self.clear_depreciation_schedule()
start = self.clear_depreciation_schedule()
for finance_book in self.get('finance_books'):
self.validate_asset_finance_books(finance_book)
# value_after_depreciation - current Asset value
if self.docstatus == 1 and d.value_after_depreciation:
value_after_depreciation = flt(d.value_after_depreciation)
if self.docstatus == 1 and finance_book.value_after_depreciation:
value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked)
has_pro_rata = self.check_is_pro_rata(d)
has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
for n in range(start, number_of_pending_depreciations):
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation))
schedule_date = add_months(finance_book.depreciation_start_date,
n * cint(finance_book.frequency_of_depreciation))
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
# if asset is being sold
if date_of_sale:
from_date = self.get_from_date(d.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, date_of_sale)
if depreciation_amount > 0:
self.append("schedules", {
"schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
break
# For first row
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date)
from_date = add_days(self.available_for_use_date, -1) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, finance_book.depreciation_start_date)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date,
(n + self.number_of_depreciations_booked) * cint(d.frequency_of_depreciation))
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(d,
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, d.finance_book)
depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
@@ -287,10 +289,10 @@ class Asset(AccountsController):
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != d.expected_value_after_useful_life)
or value_after_depreciation < d.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0:
@@ -300,7 +302,7 @@ class Asset(AccountsController):
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else d.frequency_of_depreciation
else finance_book.frequency_of_depreciation
for r in range(month_range):
if (has_pro_rata and n == 0):
@@ -326,27 +328,52 @@ class Asset(AccountsController):
self.append("schedules", {
"schedule_date": date,
"depreciation_amount": amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
else:
self.append("schedules", {
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
# used when depreciation schedule needs to be modified due to increase in asset life
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
def clear_depreciation_schedule(self):
start = 0
for n in range(len(self.schedules)):
if not self.schedules[n].journal_entry:
del self.schedules[n:]
start = n
break
start = []
num_of_depreciations_completed = 0
depr_schedule = []
for schedule in self.get('schedules'):
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to ensure that start will only be updated once for each FB
if len(start) == (int(schedule.finance_book_id) - 1):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
if len(start) == (len(self.finance_books) - 1):
start.append(num_of_depreciations_completed)
# when the Depreciation Schedule is being created for the first time
if start == []:
start = [0] * len(self.finance_books)
else:
self.schedules = depr_schedule
return start
def get_from_date(self, finance_book):
@@ -363,7 +390,9 @@ class Asset(AccountsController):
if from_date:
return from_date
return self.available_for_use_date
# since depr for available_for_use_date is not yet booked
return add_days(self.available_for_use_date, -1)
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):

View File

@@ -207,9 +207,9 @@ class TestAsset(AssetSetup):
self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold")
expected_gle = (
("_Test Accumulated Depreciations - _TC", 20392.16, 0.0),
("_Test Accumulated Depreciations - _TC", 20490.2, 0.0),
("_Test Fixed Asset - _TC", 0.0, 100000.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0),
("_Test Gain/Loss on Asset Disposal - _TC", 54509.8, 0.0),
("Debtors - _TC", 25000.0, 0.0)
)
@@ -491,10 +491,10 @@ class TestDepreciationMethods(AssetSetup):
)
expected_schedules = [
["2030-12-31", 27534.25, 27534.25],
["2031-12-31", 30000.0, 57534.25],
["2032-12-31", 30000.0, 87534.25],
["2033-01-30", 2465.75, 90000.0]
['2030-12-31', 27616.44, 27616.44],
['2031-12-31', 30000.0, 57616.44],
['2032-12-31', 30000.0, 87616.44],
['2033-01-30', 2383.56, 90000.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -544,10 +544,10 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 28493.15, 28493.15],
["2031-12-31", 35753.43, 64246.58],
["2032-12-31", 17876.71, 82123.29],
["2033-06-06", 5376.71, 87500.0]
['2030-12-31', 28630.14, 28630.14],
['2031-12-31', 35684.93, 64315.07],
['2032-12-31', 17842.47, 82157.54],
['2033-06-06', 5342.46, 87500.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -580,10 +580,10 @@ class TestDepreciationMethods(AssetSetup):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 11780.82, 11780.82],
["2031-12-31", 44109.59, 55890.41],
["2032-12-31", 22054.8, 77945.21],
["2033-07-12", 9554.79, 87500.0]
["2030-12-31", 11849.32, 11849.32],
["2031-12-31", 44075.34, 55924.66],
["2032-12-31", 22037.67, 77962.33],
["2033-07-12", 9537.67, 87500.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -642,7 +642,7 @@ class TestDepreciationBasics(AssetSetup):
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = getdate("2019-12-31"),
available_for_use_date = getdate("2020-01-01"),
total_number_of_depreciations = 3,
expected_value_after_useful_life = 10000,
depreciation_start_date = getdate("2020-07-01"),
@@ -653,7 +653,7 @@ class TestDepreciationBasics(AssetSetup):
["2020-07-01", 15000, 15000],
["2021-07-01", 30000, 45000],
["2022-07-01", 30000, 75000],
["2022-12-31", 15000, 90000]
["2023-01-01", 15000, 90000]
]
for i, schedule in enumerate(asset.schedules):
@@ -976,6 +976,82 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(len(asset.schedules), 1)
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.submit()
post_depreciation_entries(date="2020-04-01")
asset.load_from_db()
asset.clear_depreciation_schedule()
self.assertEqual(len(asset.schedules), 6)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, "1")
else:
self.assertEqual(schedule.finance_book_id, "2")
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.save()
self.assertEqual(len(asset.schedules), 9)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, 1)
else:
self.assertEqual(schedule.finance_book_id, 2)
def test_depreciation_entry_cancellation(self):
asset = create_asset(
item_code = "Macbook Pro",

View File

@@ -256,11 +256,7 @@ class StockController(AccountsController):
for d in self.items:
if not d.batch_no: continue
serial_nos = [sr.name for sr in frappe.get_all("Serial No",
{'batch_no': d.batch_no, 'status': 'Inactive'})]
if serial_nos:
frappe.db.set_value("Serial No", { 'name': ['in', serial_nos] }, "batch_no", None)
frappe.db.set_value("Serial No", {"batch_no": d.batch_no, "status": "Inactive"}, "batch_no", None)
d.batch_no = None
d.db_set("batch_no", None)

View File

@@ -276,10 +276,29 @@ def guess_territory():
def decorate_quotation_doc(doc):
for d in doc.get("items", []):
item_code = d.item_code
fields = ["web_item_name", "thumbnail", "website_image", "description", "route"]
# Variant Item
if not frappe.db.exists("Website Item", {"item_code": item_code}):
variant_data = frappe.db.get_values(
"Item",
filters={"item_code": item_code},
fieldname=["variant_of", "item_name", "image"],
as_dict=True
)[0]
item_code = variant_data.variant_of
fields = fields[1:]
d.web_item_name = variant_data.item_name
if variant_data.image: # get image from variant or template web item
d.thumbnail = variant_data.image
fields = fields[2:]
d.update(frappe.db.get_value(
"Website Item",
{"item_code": d.item_code},
["web_item_name", "thumbnail", "website_image", "description", "route"],
{"item_code": item_code},
fields,
as_dict=True)
)

View File

@@ -9,8 +9,13 @@ from frappe.utils import add_months, nowdate
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.shopping_cart.cart import _get_cart_quotation, get_party, update_cart
from erpnext.tests.utils import create_test_contact_and_address
from erpnext.e_commerce.shopping_cart.cart import (
_get_cart_quotation,
get_cart_quotation,
get_party,
update_cart,
)
from erpnext.tests.utils import change_settings, create_test_contact_and_address
class TestShoppingCart(unittest.TestCase):
@@ -34,6 +39,7 @@ class TestShoppingCart(unittest.TestCase):
make_website_item(frappe.get_cached_doc("Item", "_Test Item 2"))
def tearDown(self):
frappe.db.rollback()
frappe.set_user("Administrator")
self.disable_shopping_cart()
@@ -128,6 +134,43 @@ class TestShoppingCart(unittest.TestCase):
self.remove_test_quotation(quotation)
@change_settings("E Commerce Settings",{
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
def test_add_item_variant_without_web_item_to_cart(self):
"Test adding Variants having no Website Items in cart via Template Web Item."
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.stock.doctype.item.test_item import make_item
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small", "Test Colour": "Red"
})
variant.save()
make_website_item(template_item) # publish template not variant
update_cart("Test-Tshirt-Temp-S-R", 1)
cart = get_cart_quotation() # test if cart page gets data without errors
doc = cart.get("doc")
self.assertEqual(doc.get("items")[0].item_name, "Test-Tshirt-Temp-S-R")
# test if items are rendered without error
frappe.render_template("templates/includes/cart/cart_items.html", cart)
def create_tax_rule(self):
tax_rule = frappe.get_test_records("Tax Rule")[0]
try:

View File

@@ -3,24 +3,26 @@ import unittest
import frappe
from erpnext.controllers.item_variant import create_variant
from erpnext.e_commerce.doctype.e_commerce_settings.test_e_commerce_settings import (
setup_e_commerce_settings,
)
from erpnext.e_commerce.doctype.website_item.website_item import make_website_item
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.tests.utils import ERPNextTestCase
test_dependencies = ["Item"]
class TestVariantSelector(unittest.TestCase):
class TestVariantSelector(ERPNextTestCase):
def setUp(self) -> None:
self.template_item = make_item("Test-Tshirt-Temp", {
@classmethod
def setUpClass(cls):
template_item = make_item("Test-Tshirt-Temp", {
"has_variant": 1,
"variant_based_on": "Item Attribute",
"attributes": [
{
"attribute": "Test Size"
},
{
"attribute": "Test Colour"
}
{"attribute": "Test Size"},
{"attribute": "Test Colour"}
]
})
@@ -28,19 +30,16 @@ class TestVariantSelector(unittest.TestCase):
for size in ("Large", "Medium",):
for colour in ("Red", "Green",):
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": size,
"Test Colour": colour
"Test Size": size, "Test Colour": colour
})
variant.save()
variant = create_variant("Test-Tshirt-Temp", {
"Test Size": "Small",
"Test Colour": "Red"
"Test Size": "Small", "Test Colour": "Red"
})
variant.save()
def tearDown(self):
frappe.db.rollback()
make_website_item(template_item) # publish template not variants
def test_item_attributes(self):
"""
@@ -51,8 +50,6 @@ class TestVariantSelector(unittest.TestCase):
"""
from erpnext.e_commerce.variant_selector.utils import get_attributes_and_values
make_website_item(self.template_item) # publish template not variants
attr_data = get_attributes_and_values("Test-Tshirt-Temp")
self.assertEqual(attr_data[0]["attribute"], "Test Size")
@@ -72,7 +69,7 @@ class TestVariantSelector(unittest.TestCase):
self.assertEqual(len(attr_data[0]["values"]), 2) # ['Medium', 'Large']
# teardown
small_variant.disabled = 1
small_variant.disabled = 0
small_variant.save()
def test_next_item_variant_values(self):
@@ -84,8 +81,6 @@ class TestVariantSelector(unittest.TestCase):
There is a ** Small-Red ** Tshirt. No other colour in this size.
On selecting ** Small **, only ** Red ** should be selectable next.
"""
from erpnext.e_commerce.variant_selector.utils import get_next_attribute_and_values
next_values = get_next_attribute_and_values("Test-Tshirt-Temp", selected_attributes={"Test Size": "Small"})
next_colours = next_values["valid_options_for_attributes"]["Test Colour"]
filtered_items = next_values["filtered_items"]
@@ -94,3 +89,31 @@ class TestVariantSelector(unittest.TestCase):
self.assertEqual(next_colours.pop(), "Red")
self.assertEqual(len(filtered_items), 1)
self.assertEqual(filtered_items.pop(), "Test-Tshirt-Temp-S-R")
def test_exact_match_with_price(self):
"""
Test price fetching and matching of variant without Website Item
"""
from erpnext.e_commerce.doctype.website_item.test_website_item import make_web_item_price
frappe.set_user("Administrator")
setup_e_commerce_settings({
"company": "_Test Company",
"enabled": 1,
"default_customer_group": "_Test Customer Group",
"price_list": "_Test Price List India",
"show_price": 1
})
make_web_item_price(item_code="Test-Tshirt-Temp-S-R", price_list_rate=100)
next_values = get_next_attribute_and_values(
"Test-Tshirt-Temp",
selected_attributes={"Test Size": "Small", "Test Colour": "Red"}
)
print(">>>>", next_values)
price_info = next_values["product_info"]["price"]
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(next_values["exact_match"][0],"Test-Tshirt-Temp-S-R")
self.assertEqual(price_info["price_list_rate"], 100.0)
self.assertEqual(price_info["formatted_price_sales_uom"], "₹ 100.00")

View File

@@ -1,7 +1,12 @@
import frappe
from frappe.utils import cint
from erpnext.e_commerce.doctype.e_commerce_settings.e_commerce_settings import (
get_shopping_cart_settings,
)
from erpnext.e_commerce.shopping_cart.cart import _set_price_list
from erpnext.e_commerce.variant_selector.item_variants_cache import ItemVariantsCacheManager
from erpnext.utilities.product import get_price
def get_item_codes_by_attributes(attribute_filters, template_item_code=None):
@@ -143,14 +148,13 @@ def get_next_attribute_and_values(item_code, selected_attributes):
filtered_items_count = len(filtered_items)
# get product info if exact match
from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
# from erpnext.e_commerce.shopping_cart.product_info import get_product_info_for_website
if exact_match:
data = get_product_info_for_website(exact_match[0])
product_info = data.product_info
cart_settings = get_shopping_cart_settings()
product_info = get_item_variant_price_dict(exact_match[0], cart_settings)
if product_info:
product_info["allow_items_not_in_stock"] = cint(data.cart_settings.allow_items_not_in_stock)
if not data.cart_settings.show_price:
product_info = None
product_info["allow_items_not_in_stock"] = cint(cart_settings.allow_items_not_in_stock)
else:
product_info = None
@@ -195,3 +199,20 @@ def get_item_attributes(item_code):
return attributes
def get_item_variant_price_dict(item_code, cart_settings):
if cart_settings.enabled and cart_settings.show_price:
is_guest = frappe.session.user == "Guest"
# Show Price if logged in.
# If not logged in, check if price is hidden for guest.
if not is_guest or not cart_settings.hide_price_for_guest:
price_list = _set_price_list(cart_settings, None)
price = get_price(
item_code,
price_list,
cart_settings.default_customer_group,
cart_settings.company
)
return {"price": price}
return None

View File

@@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.utils import format_date
from frappe.utils.data import add_days, formatdate, today
from erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule import (
@@ -82,6 +83,13 @@ class TestMaintenanceSchedule(unittest.TestCase):
#checks if visit status is back updated in schedule
self.assertTrue(ms.schedules[1].completion_status, "Partially Completed")
self.assertEqual(format_date(visit.mntc_date), format_date(ms.schedules[1].actual_date))
#checks if visit status is updated on cancel
visit.cancel()
ms.reload()
self.assertTrue(ms.schedules[1].completion_status, "Pending")
self.assertEqual(ms.schedules[1].actual_date, None)
def test_serial_no_filters(self):
# Without serial no. set in schedule -> returns None

View File

@@ -4,7 +4,7 @@
import frappe
from frappe import _
from frappe.utils import get_datetime
from frappe.utils import format_date, get_datetime
from erpnext.utilities.transaction_base import TransactionBase
@@ -28,20 +28,24 @@ class MaintenanceVisit(TransactionBase):
if item_ref:
start_date, end_date = frappe.db.get_value('Maintenance Schedule Item', item_ref, ['start_date', 'end_date'])
if get_datetime(self.mntc_date) < get_datetime(start_date) or get_datetime(self.mntc_date) > get_datetime(end_date):
frappe.throw(_("Date must be between {0} and {1}").format(start_date, end_date))
frappe.throw(_("Date must be between {0} and {1}")
.format(format_date(start_date), format_date(end_date)))
def validate(self):
self.validate_serial_no()
self.validate_maintenance_date()
self.validate_purpose_table()
def update_completion_status(self):
def update_status_and_actual_date(self, cancel=False):
status = "Pending"
actual_date = None
if not cancel:
status = self.completion_status
actual_date = self.mntc_date
if self.maintenance_schedule_detail:
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', self.completion_status)
def update_actual_date(self):
if self.maintenance_schedule_detail:
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', self.mntc_date)
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'completion_status', status)
frappe.db.set_value('Maintenance Schedule Detail', self.maintenance_schedule_detail, 'actual_date', actual_date)
def update_customer_issue(self, flag):
if not self.maintenance_schedule:
@@ -102,12 +106,12 @@ class MaintenanceVisit(TransactionBase):
def on_submit(self):
self.update_customer_issue(1)
frappe.db.set(self, 'status', 'Submitted')
self.update_completion_status()
self.update_actual_date()
self.update_status_and_actual_date()
def on_cancel(self):
self.check_if_last_visit()
frappe.db.set(self, 'status', 'Cancelled')
self.update_status_and_actual_date(cancel=True)
def on_update(self):
pass

View File

@@ -337,9 +337,11 @@ erpnext.patches.v13_0.item_naming_series_not_mandatory
erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.wipe_serial_no_field_for_0_qty
erpnext.patches.v13_0.disable_ksa_print_format_for_others # 16-12-2021
erpnext.patches.v13_0.update_tax_category_for_rcm
erpnext.patches.v13_0.convert_to_website_item_in_item_card_group_template
erpnext.patches.v13_0.agriculture_deprecation_warning
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
erpnext.patches.v13_0.hospitality_deprecation_warning
erpnext.patches.v13_0.delete_bank_reconciliation_detail

View File

@@ -0,0 +1,13 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
if frappe.db.exists('DocType', 'Bank Reconciliation Detail') and \
frappe.db.exists('DocType', 'Bank Clearance Detail'):
frappe.delete_doc("DocType", 'Bank Reconciliation Detail', force=1)

View File

@@ -0,0 +1,18 @@
import frappe
def execute():
doctype = "Stock Reconciliation Item"
if not frappe.db.has_column(doctype, "current_serial_no"):
# nothing to fix if column doesn't exist
return
sr_item = frappe.qb.DocType(doctype)
(frappe.qb
.update(sr_item)
.set(sr_item.current_serial_no, None)
.where(sr_item.current_qty == 0)
).run()

View File

@@ -5,7 +5,7 @@ import datetime
import unittest
import frappe
from frappe.utils import add_months, now_datetime, nowdate
from frappe.utils import add_months, add_to_date, now_datetime, nowdate
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -151,6 +151,27 @@ class TestTimesheet(unittest.TestCase):
settings.ignore_employee_time_overlap = initial_setting
settings.save()
def test_to_time(self):
emp = make_employee("test_employee_6@salary.com")
from_time = now_datetime()
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = emp
timesheet.append(
'time_logs',
{
"billable": 1,
"activity_type": "_Test Activity Type",
"from_time": from_time,
"hours": 2,
"company": "_Test Company"
}
)
timesheet.save()
to_time = timesheet.time_logs[0].to_time
self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True))
def make_salary_structure_for_timesheet(employee, company=None):
salary_structure_name = "Timesheet Salary Structure Test"

View File

@@ -7,7 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, getdate, time_diff_in_hours
from frappe.utils import add_to_date, flt, getdate, time_diff_in_hours
from erpnext.controllers.queries import get_match_cond
from erpnext.hr.utils import validate_active_employee
@@ -136,10 +136,19 @@ class Timesheet(Document):
def validate_time_logs(self):
for data in self.get('time_logs'):
self.set_to_time(data)
self.validate_overlap(data)
self.set_project(data)
self.validate_project(data)
def set_to_time(self, data):
if not (data.from_time and data.hours):
return
_to_time = add_to_date(data.from_time, hours=data.hours, as_datetime=True)
if data.to_time != _to_time:
data.to_time = _to_time
def validate_overlap(self, data):
settings = frappe.get_single('Projects Settings')
self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap)

View File

@@ -23,19 +23,24 @@ def execute(filters=None):
row = []
outstanding_amt = get_customer_outstanding(d.name, filters.get("company"),
ignore_outstanding_sales_order=d.bypass_credit_limit_check_at_sales_order)
ignore_outstanding_sales_order=d.bypass_credit_limit_check)
credit_limit = get_credit_limit(d.name, filters.get("company"))
bal = flt(credit_limit) - flt(outstanding_amt)
if customer_naming_type == "Naming Series":
row = [d.name, d.customer_name, credit_limit, outstanding_amt, bal,
d.bypass_credit_limit_check, d.is_frozen,
d.disabled]
row = [
d.name, d.customer_name, credit_limit,
outstanding_amt, bal, d.bypass_credit_limit_check,
d.is_frozen, d.disabled
]
else:
row = [d.name, credit_limit, outstanding_amt, bal,
d.bypass_credit_limit_check_at_sales_order, d.is_frozen, d.disabled]
row = [
d.name, credit_limit, outstanding_amt, bal,
d.bypass_credit_limit_check, d.is_frozen,
d.disabled
]
if credit_limit:
data.append(row)

View File

@@ -347,14 +347,6 @@ class Item(Document):
frappe.throw(_("Barcode {0} is not a valid {1} code").format(
item_barcode.barcode, item_barcode.barcode_type), InvalidBarcode)
if item_barcode.barcode != item_barcode.name:
# if barcode is getting updated , the row name has to reset.
# Delete previous old row doc and re-enter row as if new to reset name in db.
item_barcode.set("__islocal", True)
item_barcode_entry_name = item_barcode.name
item_barcode.name = None
frappe.delete_doc("Item Barcode", item_barcode_entry_name)
def validate_warehouse_for_reorder(self):
'''Validate Reorder level table for duplicate and conditional mandatory'''
warehouse = []

View File

@@ -6,7 +6,7 @@
import frappe
from frappe.utils import add_days, flt, nowdate, nowtime, random_string
from frappe.utils import add_days, cstr, flt, nowdate, nowtime, random_string
from erpnext.accounts.utils import get_stock_and_account_balance
from erpnext.stock.doctype.item.test_item import create_item
@@ -439,8 +439,8 @@ class TestStockReconciliation(ERPNextTestCase):
self.assertRaises(frappe.ValidationError, sr.submit)
def test_serial_no_cancellation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1)
if not item.has_serial_no:
item.has_serial_no = 1
@@ -466,6 +466,31 @@ class TestStockReconciliation(ERPNextTestCase):
self.assertEqual(len(active_sr_no), 10)
def test_serial_no_creation_and_inactivation(self):
item = create_item("_TestItemCreatedWithStockReco", is_stock_item=1)
if not item.has_serial_no:
item.has_serial_no = 1
item.save()
item_code = item.name
warehouse = "_Test Warehouse - _TC"
sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse,
serial_no="SR-CREATED-SR-NO", qty=1, do_not_submit=True, rate=100)
sr.save()
self.assertEqual(cstr(sr.items[0].current_serial_no), "")
sr.submit()
active_sr_no = frappe.get_all("Serial No",
filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
self.assertEqual(len(active_sr_no), 1)
sr.cancel()
active_sr_no = frappe.get_all("Serial No",
filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"})
self.assertEqual(len(active_sr_no), 0)
def create_batch_item_with_batch(item_name, batch_id):
batch_item_doc = create_item(item_name, is_stock_item=1)
if not batch_item_doc.has_batch_no:

View File

@@ -104,7 +104,7 @@ def get_stock_balance(item_code, warehouse, posting_date=None, posting_time=None
serial_nos = get_serial_nos_data_after_transactions(args)
return ((last_entry.qty_after_transaction, last_entry.valuation_rate, serial_nos)
if last_entry else (0.0, 0.0, 0.0))
if last_entry else (0.0, 0.0, None))
else:
return (last_entry.qty_after_transaction, last_entry.valuation_rate) if last_entry else (0.0, 0.0)
else:

View File

@@ -214,7 +214,7 @@ class ItemConfigure {
? `<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
<div><div>
${one_item}
${product_info && product_info.price && !$.isEmptyObject()
${product_info && product_info.price && !$.isEmptyObject(product_info.price)
? '(' + product_info.price.formatted_price_sales_uom + ')'
: ''
}