Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into mergify/bp/version-13-hotfix/pr-28675

This commit is contained in:
Saqib Ansari
2021-12-06 15:49:02 +05:30
24 changed files with 931 additions and 582 deletions

View File

@@ -57,7 +57,8 @@ class GLEntry(Document):
# Update outstanding amt on against voucher # Update outstanding amt on against voucher
if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
and self.against_voucher and self.flags.update_outstanding == 'Yes'): and self.against_voucher and self.flags.update_outstanding == 'Yes'
and not frappe.flags.is_reverse_depr_entry):
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type, update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
self.against_voucher) self.against_voucher)

View File

@@ -57,7 +57,10 @@ class JournalEntry(AccountsController):
if not frappe.flags.in_import: if not frappe.flags.in_import:
self.validate_total_debit_and_credit() self.validate_total_debit_and_credit()
if not frappe.flags.is_reverse_depr_entry:
self.validate_against_jv() self.validate_against_jv()
self.validate_stock_accounts()
self.validate_reference_doc() self.validate_reference_doc()
if self.docstatus == 0: if self.docstatus == 0:
self.set_against_account() self.set_against_account()
@@ -68,7 +71,6 @@ class JournalEntry(AccountsController):
self.validate_empty_accounts_table() self.validate_empty_accounts_table()
self.set_account_and_party_balance() self.set_account_and_party_balance()
self.validate_inter_company_accounts() self.validate_inter_company_accounts()
self.validate_stock_accounts()
if self.docstatus == 0: if self.docstatus == 0:
self.apply_tax_withholding() self.apply_tax_withholding()

View File

@@ -36,7 +36,7 @@ from erpnext.assets.doctype.asset.depreciation import (
get_disposal_account_and_cost_center, get_disposal_account_and_cost_center,
get_gl_entries_on_asset_disposal, get_gl_entries_on_asset_disposal,
get_gl_entries_on_asset_regain, get_gl_entries_on_asset_regain,
post_depreciation_entries, make_depreciation_entry,
) )
from erpnext.controllers.selling_controller import SellingController from erpnext.controllers.selling_controller import SellingController
from erpnext.healthcare.utils import manage_invoice_submit_cancel from erpnext.healthcare.utils import manage_invoice_submit_cancel
@@ -943,6 +943,7 @@ class SalesInvoice(SellingController):
asset.db_set("disposal_date", None) asset.db_set("disposal_date", None)
if asset.calculate_depreciation: if asset.calculate_depreciation:
self.reverse_depreciation_entry_made_after_sale(asset)
self.reset_depreciation_schedule(asset) self.reset_depreciation_schedule(asset)
else: else:
@@ -1006,22 +1007,20 @@ class SalesInvoice(SellingController):
def depreciate_asset(self, asset): def depreciate_asset(self, asset):
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
asset.prepare_depreciation_data(self.posting_date) asset.prepare_depreciation_data(date_of_sale=self.posting_date)
asset.save() asset.save()
post_depreciation_entries(self.posting_date) make_depreciation_entry(asset.name, self.posting_date)
def reset_depreciation_schedule(self, asset): def reset_depreciation_schedule(self, asset):
asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_validate_update_after_submit = True
# recreate original depreciation schedule of the asset # recreate original depreciation schedule of the asset
asset.prepare_depreciation_data() asset.prepare_depreciation_data(date_of_return=self.posting_date)
self.modify_depreciation_schedule_for_asset_repairs(asset) self.modify_depreciation_schedule_for_asset_repairs(asset)
asset.save() asset.save()
self.delete_depreciation_entry_made_after_sale(asset)
def modify_depreciation_schedule_for_asset_repairs(self, asset): def modify_depreciation_schedule_for_asset_repairs(self, asset):
asset_repairs = frappe.get_all( asset_repairs = frappe.get_all(
'Asset Repair', 'Asset Repair',
@@ -1035,7 +1034,7 @@ class SalesInvoice(SellingController):
asset_repair.modify_depreciation_schedule() asset_repair.modify_depreciation_schedule()
asset.prepare_depreciation_data() asset.prepare_depreciation_data()
def delete_depreciation_entry_made_after_sale(self, asset): def reverse_depreciation_entry_made_after_sale(self, asset):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
@@ -1050,11 +1049,19 @@ class SalesInvoice(SellingController):
row += 1 row += 1
if schedule.schedule_date == posting_date_of_original_invoice: if schedule.schedule_date == posting_date_of_original_invoice:
if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice): if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \
or self.sale_happens_in_the_future(posting_date_of_original_invoice):
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
reverse_journal_entry.posting_date = nowdate() reverse_journal_entry.posting_date = nowdate()
frappe.flags.is_reverse_depr_entry = True
reverse_journal_entry.submit() reverse_journal_entry.submit()
frappe.flags.is_reverse_depr_entry = False
asset.flags.ignore_validate_update_after_submit = True
schedule.journal_entry = None
asset.save()
def get_posting_date_of_sales_invoice(self): def get_posting_date_of_sales_invoice(self):
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
@@ -1069,6 +1076,12 @@ class SalesInvoice(SellingController):
return True return True
return False return False
def sale_happens_in_the_future(self, posting_date_of_original_invoice):
if posting_date_of_original_invoice > getdate():
return True
return False
@property @property
def enable_discount_accounting(self): def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"): if not hasattr(self, "_enable_discount_accounting"):

View File

@@ -2192,9 +2192,9 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0) enable_discount_accounting(enable=0)
def test_asset_depreciation_on_sale(self): def test_asset_depreciation_on_sale_with_pro_rata(self):
""" """
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on Sept 30. Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
""" """
create_asset_data() create_asset_data()
@@ -2207,7 +2207,7 @@ class TestSalesInvoice(unittest.TestCase):
expected_values = [ expected_values = [
["2020-06-30", 1311.48, 1311.48], ["2020-06-30", 1311.48, 1311.48],
["2021-06-30", 20000.0, 21311.48], ["2021-06-30", 20000.0, 21311.48],
["2021-09-30", 3966.76, 25278.24] ["2021-09-30", 5041.1, 26352.58]
] ]
for i, schedule in enumerate(asset.schedules): for i, schedule in enumerate(asset.schedules):
@@ -2216,6 +2216,59 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount) self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry) self.assertTrue(schedule.journal_entry)
def test_asset_depreciation_on_sale_without_pro_rata(self):
"""
Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale.
"""
create_asset_data()
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1,
available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3,
expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1)
post_depreciation_entries(getdate("2021-09-30"))
create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31"))
asset.load_from_db()
expected_values = [
["2020-12-31", 30000, 30000],
["2021-12-31", 30000, 60000]
]
for i, schedule in enumerate(asset.schedules):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertTrue(schedule.journal_entry)
def test_depreciation_on_return_of_sold_asset(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
create_asset_data()
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
post_depreciation_entries(getdate("2021-09-30"))
si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30"))
return_si = make_return_doc("Sales Invoice", si.name)
return_si.submit()
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]
]
for i, schedule in enumerate(asset.schedules):
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
def test_sales_invoice_against_supplier(self): def test_sales_invoice_against_supplier(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import ( from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer, make_customer,

View File

@@ -746,7 +746,6 @@
"fieldname": "asset", "fieldname": "asset",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Asset", "label": "Asset",
"no_copy": 1,
"options": "Asset" "options": "Asset"
}, },
{ {

View File

@@ -73,12 +73,12 @@ class Asset(AccountsController):
if self.is_existing_asset and self.purchase_invoice: if self.is_existing_asset and self.purchase_invoice:
frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name)) frappe.throw(_("Purchase Invoice cannot be made against an existing asset {0}").format(self.name))
def prepare_depreciation_data(self, date_of_sale=None): def prepare_depreciation_data(self, date_of_sale=None, date_of_return=None):
if self.calculate_depreciation: if self.calculate_depreciation:
self.value_after_depreciation = 0 self.value_after_depreciation = 0
self.set_depreciation_rate() self.set_depreciation_rate()
self.make_depreciation_schedule(date_of_sale) self.make_depreciation_schedule(date_of_sale)
self.set_accumulated_depreciation(date_of_sale) self.set_accumulated_depreciation(date_of_sale, date_of_return)
else: else:
self.finance_books = [] self.finance_books = []
self.value_after_depreciation = (flt(self.gross_purchase_amount) - self.value_after_depreciation = (flt(self.gross_purchase_amount) -
@@ -180,7 +180,7 @@ class Asset(AccountsController):
d.precision("rate_of_depreciation")) d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self, date_of_sale): def make_depreciation_schedule(self, date_of_sale):
if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules: if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.get('schedules'):
self.schedules = [] self.schedules = []
if not self.available_for_use_date: if not self.available_for_use_date:
@@ -229,6 +229,7 @@ class Asset(AccountsController):
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount, depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
from_date, date_of_sale) from_date, date_of_sale)
if depreciation_amount > 0:
self.append("schedules", { self.append("schedules", {
"schedule_date": date_of_sale, "schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount, "depreciation_amount": depreciation_amount,
@@ -236,6 +237,7 @@ class Asset(AccountsController):
"finance_book": d.finance_book, "finance_book": d.finance_book,
"finance_book_id": d.idx "finance_book_id": d.idx
}) })
break break
# For first row # For first row
@@ -254,11 +256,15 @@ class Asset(AccountsController):
self.to_date = add_months(self.available_for_use_date, 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(d.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(d,
depreciation_amount, schedule_date, self.to_date) depreciation_amount, schedule_date, self.to_date)
monthly_schedule_date = add_months(schedule_date, 1) depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, d.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days) schedule_date = add_days(schedule_date, days)
last_schedule_date = schedule_date last_schedule_date = schedule_date
@@ -424,7 +430,7 @@ class Asset(AccountsController):
if len(self.finance_books) == 1: if len(self.finance_books) == 1:
return True return True
def set_accumulated_depreciation(self, date_of_sale=None, ignore_booked_entry = False): def set_accumulated_depreciation(self, date_of_sale=None, date_of_return=None, ignore_booked_entry = False):
straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line'] straight_line_idx = [d.idx for d in self.get("schedules") if d.depreciation_method == 'Straight Line']
finance_books = [] finance_books = []
@@ -441,7 +447,7 @@ class Asset(AccountsController):
value_after_depreciation -= flt(depreciation_amount) value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line # for the last row, if depreciation method = Straight Line
if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale: if straight_line_idx and i == max(straight_line_idx) - 1 and not date_of_sale and not date_of_return:
book = self.get('finance_books')[cint(d.finance_book_id) - 1] book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation - depreciation_amount += flt(value_after_depreciation -
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount")) flt(book.expected_value_after_useful_life), d.precision("depreciation_amount"))

File diff suppressed because it is too large Load Diff

View File

@@ -304,6 +304,7 @@ erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.add_custom_field_for_south_africa #2 erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record erpnext.patches.v13_0.rename_discharge_ordered_date_in_ip_record
erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.migrate_stripe_api erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings") execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")

View File

@@ -3,9 +3,9 @@
import frappe import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
import erpnext import erpnext
from erpnext.regional.india.setup import setup
def execute(): def execute():
@@ -30,7 +30,14 @@ def execute():
frappe.reload_doc('Regional', 'Report', report) frappe.reload_doc('Regional', 'Report', report)
if erpnext.get_region() == "India": if erpnext.get_region() == "India":
setup(patch=True) create_custom_field('Salary Component',
dict(fieldname='component_type',
label='Component Type',
fieldtype='Select',
insert_after='description',
options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax',
depends_on='eval:doc.type == "Deduction"')
)
if frappe.db.exists("Salary Component", "Income Tax"): if frappe.db.exists("Salary Component", "Income Tax"):
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1) frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)

View File

@@ -0,0 +1,16 @@
import frappe
from frappe import _
def execute():
frappe.reload_doctype("Selling Settings")
selling_settings = frappe.get_single("Selling Settings")
if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
selling_settings.customer_group = None
if selling_settings.territory in (_("All Territories"), "All Territories"):
selling_settings.territory = None
selling_settings.flags.ignore_mandatory=True
selling_settings.save(ignore_permissions=True)

View File

@@ -1,7 +1,6 @@
import io import io
import os import os
from base64 import b64encode from base64 import b64encode
from urllib.parse import quote
import frappe import frappe
from frappe import _ from frappe import _
@@ -102,9 +101,10 @@ def create_qr_code(doc, method):
url = qr_create(base64_string, error='L') url = qr_create(base64_string, error='L')
url.png(qr_image, scale=2, quiet_zone=1) url.png(qr_image, scale=2, quiet_zone=1)
urlencoded_name = quote(doc.name) name = frappe.generate_hash(doc.name, 5)
# making file # making file
filename = f"QR-CODE-{urlencoded_name}.png".replace(os.path.sep, "__") filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
_file = frappe.get_doc({ _file = frappe.get_doc({
"doctype": "File", "doctype": "File",
"file_name": filename, "file_name": filename,

View File

@@ -8,7 +8,6 @@ import frappe
from frappe.custom.doctype.property_setter.property_setter import make_property_setter from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint from frappe.utils import cint
from frappe.utils.nestedset import get_root_of
class SellingSettings(Document): class SellingSettings(Document):
@@ -37,9 +36,3 @@ class SellingSettings(Document):
editable_bundle_item_rates = cint(self.editable_bundle_item_rates) editable_bundle_item_rates = cint(self.editable_bundle_item_rates)
make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False) make_property_setter("Packed Item", "rate", "read_only", not(editable_bundle_item_rates), "Check", validate_fields_for_doctype=False)
def set_default_customer_group_and_territory(self):
if not self.customer_group:
self.customer_group = get_root_of('Customer Group')
if not self.territory:
self.territory = get_root_of('Territory')

View File

@@ -304,7 +304,6 @@ def set_more_defaults():
def update_selling_defaults(): def update_selling_defaults():
selling_settings = frappe.get_doc("Selling Settings") selling_settings = frappe.get_doc("Selling Settings")
selling_settings.set_default_customer_group_and_territory()
selling_settings.cust_master_name = "Customer Name" selling_settings.cust_master_name = "Customer Name"
selling_settings.so_required = "No" selling_settings.so_required = "No"
selling_settings.dn_required = "No" selling_settings.dn_required = "No"

View File

@@ -53,6 +53,7 @@ def before_tests():
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0) frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
enable_all_roles_and_domains() enable_all_roles_and_domains()
set_defaults_for_tests()
frappe.db.commit() frappe.db.commit()
@@ -127,6 +128,14 @@ def enable_all_roles_and_domains():
[d.name for d in domains]) [d.name for d in domains])
add_all_roles_to('Administrator') add_all_roles_to('Administrator')
def set_defaults_for_tests():
from frappe.utils.nestedset import get_root_of
selling_settings = frappe.get_single("Selling Settings")
selling_settings.customer_group = get_root_of("Customer Group")
selling_settings.territory = get_root_of("Territory")
selling_settings.save()
def insert_record(records): def insert_record(records):
for r in records: for r in records:

View File

@@ -4,7 +4,7 @@
import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import flt, nowdate from frappe.utils import flt
class Bin(Document): class Bin(Document):
@@ -100,33 +100,11 @@ def on_doctype_update():
def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin''' """WARNING: This function is deprecated. Inline this function instead of using it."""
from erpnext.stock.stock_ledger import repost_current_voucher
update_qty(bin_name, args) update_qty(bin_name, args)
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": args.get('item_code'),
"warehouse": args.get('warehouse'),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get('name'),
"creation": args.get('creation')
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# update qty in future sle and Validate negative qty
update_qty_in_future_sle(args, allow_negative_stock)
def get_bin_details(bin_name): def get_bin_details(bin_name):
return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty', return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',

View File

@@ -956,7 +956,7 @@
"image_field": "image", "image_field": "image",
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2021-11-30 02:33:06.572442", "modified": "2021-12-03 08:32:03.869294",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",
@@ -1023,7 +1023,7 @@
"search_fields": "item_name,description,item_group,customer_code", "search_fields": "item_name,description,item_group,customer_code",
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"show_preview_popup": 1, "show_preview_popup": 1,
"sort_field": "idx desc,modified desc", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"title_field": "item_name", "title_field": "item_name",
"track_changes": 1 "track_changes": 1

View File

@@ -496,7 +496,6 @@ class Item(Document):
def recalculate_bin_qty(self, new_name): def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock from erpnext.stock.stock_balance import repost_stock
frappe.db.auto_commit_on_many_writes = 1
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)
@@ -510,7 +509,6 @@ class Item(Document):
repost_stock(new_name, warehouse) repost_stock(new_name, warehouse)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
frappe.db.auto_commit_on_many_writes = 0
def update_bom_item_desc(self): def update_bom_item_desc(self):
if self.is_new(): if self.is_new():

View File

@@ -546,7 +546,7 @@ class StockEntry(StockController):
scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item]) scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
# Get raw materials cost from BOM if multiple material consumption entries # Get raw materials cost from BOM if multiple material consumption entries
if frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True): if not outgoing_items_cost and frappe.db.get_single_value("Manufacturing Settings", "material_consumption", cache=True):
bom_items = self.get_bom_raw_materials(finished_item_qty) bom_items = self.get_bom_raw_materials(finished_item_qty)
outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()]) outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])

View File

@@ -42,6 +42,7 @@ def get_sle(**args):
class TestStockEntry(unittest.TestCase): class TestStockEntry(unittest.TestCase):
def tearDown(self): def tearDown(self):
frappe.set_user("Administrator") frappe.set_user("Administrator")
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
def test_fifo(self): def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1) frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -583,6 +584,65 @@ class TestStockEntry(unittest.TestCase):
self.assertEqual(fg_cost, self.assertEqual(fg_cost,
flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2)) flt(rm_cost + bom_operation_cost + work_order.additional_operating_cost, 2))
def test_work_order_manufacture_with_material_consumption(self):
from erpnext.manufacturing.doctype.work_order.work_order import (
make_stock_entry as _make_stock_entry,
)
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "1")
bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item",
"is_default": 1, "docstatus": 1})
work_order = frappe.new_doc("Work Order")
work_order.update({
"company": "_Test Company",
"fg_warehouse": "_Test Warehouse 1 - _TC",
"production_item": "_Test FG Item",
"bom_no": bom_no,
"qty": 1.0,
"stock_uom": "_Test UOM",
"wip_warehouse": "_Test Warehouse - _TC"
})
work_order.insert()
work_order.submit()
make_stock_entry(item_code="_Test Item",
target="Stores - _TC", qty=10, basic_rate=5000.0)
make_stock_entry(item_code="_Test Item Home Desktop 100",
target="Stores - _TC", qty=10, basic_rate=1000.0)
s = frappe.get_doc(_make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
for d in s.get("items"):
d.s_warehouse = "Stores - _TC"
s.insert()
s.submit()
# When Stock Entry has RM and FG
s = frappe.get_doc(_make_stock_entry(work_order.name, "Manufacture", 1))
s.save()
rm_cost = 0
for d in s.get('items'):
if d.s_warehouse:
rm_cost += d.amount
fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
scrap_cost = list(filter(lambda x: x.is_scrap_item, s.get("items")))[0].amount
self.assertEqual(fg_cost,
flt(rm_cost - scrap_cost, 2))
# When Stock Entry has only FG + Scrap
s.items.pop(0)
s.items.pop(0)
s.submit()
rm_cost = 0
for d in s.get('items'):
if d.s_warehouse:
rm_cost += d.amount
self.assertEqual(rm_cost, 0)
expected_fg_cost = s.get_basic_rate_for_manufactured_item(1)
fg_cost = list(filter(lambda x: x.item_code=="_Test FG Item", s.get("items")))[0].amount
self.assertEqual(flt(fg_cost, 2), flt(expected_fg_cost, 2))
def test_variant_work_order(self): def test_variant_work_order(self):
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item", bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",

View File

@@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase):
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse) self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
self.assertEqual(child_warehouse.is_group, 0) self.assertEqual(child_warehouse.is_group, 0)
def test_warehouse_renaming(self):
create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
# Rename with abbr
if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Rename without abbr
if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Another rename with multiple dashes
if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
def test_warehouse_merging(self):
company = "_Test Company with perpetual inventory"
create_warehouse("Test Warehouse for Merging 1", company=company,
properties={"parent_warehouse": "All Warehouses - TCP1"})
create_warehouse("Test Warehouse for Merging 2", company=company,
properties={"parent_warehouse": "All Warehouses - TCP1"})
make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
qty=1, rate=100, company=company)
make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
qty=1, rate=100, company=company)
existing_bin_qty = (
cint(frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
+ cint(frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
)
frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
"Test Warehouse for Merging 2 - TCP1", merge=True)
self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
bin_qty = frappe.db.get_value("Bin",
{"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
self.assertEqual(bin_qty, existing_bin_qty)
self.assertTrue(frappe.db.get_value("Warehouse",
filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
def test_unlinking_warehouse_from_item_defaults(self): def test_unlinking_warehouse_from_item_defaults(self):
company = "_Test Company" company = "_Test Company"

View File

@@ -1,7 +1,6 @@
{ {
"actions": [], "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1,
"creation": "2013-03-07 18:50:32", "creation": "2013-03-07 18:50:32",
"description": "A logical Warehouse against which stock entries are made.", "description": "A logical Warehouse against which stock entries are made.",
"doctype": "DocType", "doctype": "DocType",
@@ -245,7 +244,7 @@
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2021-04-09 19:54:56.263965", "modified": "2021-12-03 04:40:06.414630",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Warehouse", "name": "Warehouse",

View File

@@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact
from frappe.utils import cint, flt from frappe.utils import cint, flt
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
import erpnext
from erpnext.stock import get_warehouse_account from erpnext.stock import get_warehouse_account
@@ -68,57 +67,6 @@ class Warehouse(NestedSet):
return frappe.db.sql("""select name from `tabWarehouse` return frappe.db.sql("""select name from `tabWarehouse`
where parent_warehouse = %s limit 1""", self.name) where parent_warehouse = %s limit 1""", self.name)
def before_rename(self, old_name, new_name, merge=False):
super(Warehouse, self).before_rename(old_name, new_name, merge)
# Add company abbr if not provided
new_warehouse = erpnext.encode_company_abbr(new_name, self.company)
if merge:
if not frappe.db.exists("Warehouse", new_warehouse):
frappe.throw(_("Warehouse {0} does not exist").format(new_warehouse))
if self.company != frappe.db.get_value("Warehouse", new_warehouse, "company"):
frappe.throw(_("Both Warehouse must belong to same Company"))
return new_warehouse
def after_rename(self, old_name, new_name, merge=False):
super(Warehouse, self).after_rename(old_name, new_name, merge)
new_warehouse_name = self.get_new_warehouse_name_without_abbr(new_name)
self.db_set("warehouse_name", new_warehouse_name)
if merge:
self.recalculate_bin_qty(new_name)
def get_new_warehouse_name_without_abbr(self, name):
company_abbr = frappe.get_cached_value('Company', self.company, "abbr")
parts = name.rsplit(" - ", 1)
if parts[-1].lower() == company_abbr.lower():
name = parts[0]
return name
def recalculate_bin_qty(self, new_name):
from erpnext.stock.stock_balance import repost_stock
frappe.db.auto_commit_on_many_writes = 1
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)
repost_stock_for_items = frappe.db.sql_list("""select distinct item_code
from tabBin where warehouse=%s""", new_name)
# Delete all existing bins to avoid duplicate bins for the same item and warehouse
frappe.db.sql("delete from `tabBin` where warehouse=%s", new_name)
for item_code in repost_stock_for_items:
repost_stock(item_code, new_name)
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
frappe.db.auto_commit_on_many_writes = 0
def convert_to_group_or_ledger(self): def convert_to_group_or_ledger(self):
if self.is_group: if self.is_group:
self.convert_to_ledger() self.convert_to_ledger()

View File

@@ -7,10 +7,11 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.meta import get_field_precision from frappe.model.meta import get_field_precision
from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now from frappe.utils import cint, cstr, flt, get_link_to_form, getdate, now, nowdate
from six import iteritems from six import iteritems
import erpnext import erpnext
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
from erpnext.stock.utils import ( from erpnext.stock.utils import (
get_incoming_outgoing_rate_for_cancel, get_incoming_outgoing_rate_for_cancel,
get_or_make_bin, get_or_make_bin,
@@ -18,19 +19,15 @@ from erpnext.stock.utils import (
) )
# future reposting
class NegativeStockError(frappe.ValidationError): pass class NegativeStockError(frappe.ValidationError): pass
class SerialNoExistsInFutureTransaction(frappe.ValidationError): class SerialNoExistsInFutureTransaction(frappe.ValidationError):
pass pass
_exceptions = frappe.local('stockledger_exceptions') _exceptions = frappe.local('stockledger_exceptions')
# _exceptions = []
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
from erpnext.controllers.stock_controller import future_sle_exists from erpnext.controllers.stock_controller import future_sle_exists
if sl_entries: if sl_entries:
from erpnext.stock.utils import update_bin
cancel = sl_entries[0].get("is_cancelled") cancel = sl_entries[0].get("is_cancelled")
if cancel: if cancel:
validate_cancellation(sl_entries) validate_cancellation(sl_entries)
@@ -65,7 +62,38 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc
# preserve previous_qty_after_transaction for qty reposting # preserve previous_qty_after_transaction for qty reposting
args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction") args.previous_qty_after_transaction = sle.get("previous_qty_after_transaction")
update_bin(args, allow_negative_stock, via_landed_cost_voucher) is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item:
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
update_bin_qty(bin_name, args)
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
def repost_current_voucher(args, allow_negative_stock=False, via_landed_cost_voucher=False):
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
if not args.get("posting_date"):
args["posting_date"] = nowdate()
if args.get("is_cancelled") and via_landed_cost_voucher:
return
# Reposts only current voucher SL Entries
# Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": args.get('item_code'),
"warehouse": args.get('warehouse'),
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
"sle_id": args.get('name'),
"creation": args.get('creation')
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
# update qty in future sle and Validate negative qty
update_qty_in_future_sle(args, allow_negative_stock)
def get_args_for_future_sle(row): def get_args_for_future_sle(row):
return frappe._dict({ return frappe._dict({
@@ -795,10 +823,10 @@ class update_entries_after(object):
def update_bin(self): def update_bin(self):
# update bin for each warehouse # update bin for each warehouse
for warehouse, data in iteritems(self.data): for warehouse, data in self.data.items():
bin_record = get_or_make_bin(self.item_code, warehouse) bin_name = get_or_make_bin(self.item_code, warehouse)
frappe.db.set_value('Bin', bin_record, { frappe.db.set_value('Bin', bin_name, {
"valuation_rate": data.valuation_rate, "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

View File

@@ -188,7 +188,7 @@ def get_bin(item_code, warehouse):
bin_obj.flags.ignore_permissions = True bin_obj.flags.ignore_permissions = True
return bin_obj return bin_obj
def get_or_make_bin(item_code, warehouse) -> str: def get_or_make_bin(item_code: str , warehouse: str) -> str:
bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse}) bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
if not bin_record: if not bin_record:
@@ -204,11 +204,12 @@ def get_or_make_bin(item_code, warehouse) -> str:
return bin_record return bin_record
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False): def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
"""WARNING: This function is deprecated. Inline this function instead of using it."""
from erpnext.stock.doctype.bin.bin import update_stock from erpnext.stock.doctype.bin.bin import update_stock
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item') is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item: if is_stock_item:
bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse")) bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher) update_stock(bin_name, args, allow_negative_stock, via_landed_cost_voucher)
else: else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code"))) frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))