mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 20:35:09 +00:00
Merge branch 'version-13-hotfix' of https://github.com/frappe/erpnext into mergify/bp/version-13-hotfix/pr-28675
This commit is contained in:
@@ -57,7 +57,8 @@ class GLEntry(Document):
|
||||
|
||||
# Update outstanding amt on against voucher
|
||||
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,
|
||||
self.against_voucher)
|
||||
|
||||
|
||||
@@ -57,7 +57,10 @@ class JournalEntry(AccountsController):
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
self.validate_against_jv()
|
||||
if not frappe.flags.is_reverse_depr_entry:
|
||||
self.validate_against_jv()
|
||||
self.validate_stock_accounts()
|
||||
|
||||
self.validate_reference_doc()
|
||||
if self.docstatus == 0:
|
||||
self.set_against_account()
|
||||
@@ -68,7 +71,6 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.set_account_and_party_balance()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_stock_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
@@ -36,7 +36,7 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
post_depreciation_entries,
|
||||
make_depreciation_entry,
|
||||
)
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.healthcare.utils import manage_invoice_submit_cancel
|
||||
@@ -943,6 +943,7 @@ class SalesInvoice(SellingController):
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
|
||||
else:
|
||||
@@ -1006,22 +1007,20 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def depreciate_asset(self, asset):
|
||||
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()
|
||||
|
||||
post_depreciation_entries(self.posting_date)
|
||||
make_depreciation_entry(asset.name, self.posting_date)
|
||||
|
||||
def reset_depreciation_schedule(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# 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)
|
||||
asset.save()
|
||||
|
||||
self.delete_depreciation_entry_made_after_sale(asset)
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
'Asset Repair',
|
||||
@@ -1035,7 +1034,7 @@ class SalesInvoice(SellingController):
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
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
|
||||
|
||||
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
|
||||
@@ -1050,11 +1049,19 @@ class SalesInvoice(SellingController):
|
||||
row += 1
|
||||
|
||||
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.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
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):
|
||||
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
|
||||
|
||||
@@ -1069,6 +1076,12 @@ class SalesInvoice(SellingController):
|
||||
return True
|
||||
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
|
||||
def enable_discount_accounting(self):
|
||||
if not hasattr(self, "_enable_discount_accounting"):
|
||||
|
||||
@@ -2192,9 +2192,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
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()
|
||||
@@ -2207,7 +2207,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.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):
|
||||
@@ -2216,6 +2216,59 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
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):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
|
||||
@@ -746,7 +746,6 @@
|
||||
"fieldname": "asset",
|
||||
"fieldtype": "Link",
|
||||
"label": "Asset",
|
||||
"no_copy": 1,
|
||||
"options": "Asset"
|
||||
},
|
||||
{
|
||||
|
||||
@@ -73,12 +73,12 @@ class Asset(AccountsController):
|
||||
if self.is_existing_asset and self.purchase_invoice:
|
||||
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:
|
||||
self.value_after_depreciation = 0
|
||||
self.set_depreciation_rate()
|
||||
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:
|
||||
self.finance_books = []
|
||||
self.value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||
@@ -180,7 +180,7 @@ class Asset(AccountsController):
|
||||
d.precision("rate_of_depreciation"))
|
||||
|
||||
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 = []
|
||||
|
||||
if not self.available_for_use_date:
|
||||
@@ -229,13 +229,15 @@ class Asset(AccountsController):
|
||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
||||
from_date, date_of_sale)
|
||||
|
||||
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
|
||||
})
|
||||
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
|
||||
})
|
||||
|
||||
break
|
||||
|
||||
# For first row
|
||||
@@ -254,11 +256,15 @@ class Asset(AccountsController):
|
||||
self.to_date = add_months(self.available_for_use_date,
|
||||
(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, 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)
|
||||
last_schedule_date = schedule_date
|
||||
|
||||
@@ -424,7 +430,7 @@ class Asset(AccountsController):
|
||||
if len(self.finance_books) == 1:
|
||||
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']
|
||||
finance_books = []
|
||||
|
||||
@@ -441,7 +447,7 @@ class Asset(AccountsController):
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
|
||||
# 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]
|
||||
depreciation_amount += flt(value_after_depreciation -
|
||||
flt(book.expected_value_after_useful_life), d.precision("depreciation_amount"))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -304,6 +304,7 @@ erpnext.patches.v13_0.update_recipient_email_digest
|
||||
erpnext.patches.v13_0.shopify_deprecation_warning
|
||||
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.remove_bad_selling_defaults
|
||||
erpnext.patches.v13_0.migrate_stripe_api
|
||||
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
|
||||
execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
|
||||
import erpnext
|
||||
from erpnext.regional.india.setup import setup
|
||||
|
||||
|
||||
def execute():
|
||||
@@ -30,7 +30,14 @@ def execute():
|
||||
frappe.reload_doc('Regional', 'Report', report)
|
||||
|
||||
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"):
|
||||
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)
|
||||
|
||||
16
erpnext/patches/v13_0/remove_bad_selling_defaults.py
Normal file
16
erpnext/patches/v13_0/remove_bad_selling_defaults.py
Normal 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)
|
||||
@@ -1,7 +1,6 @@
|
||||
import io
|
||||
import os
|
||||
from base64 import b64encode
|
||||
from urllib.parse import quote
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
@@ -102,9 +101,10 @@ def create_qr_code(doc, method):
|
||||
url = qr_create(base64_string, error='L')
|
||||
url.png(qr_image, scale=2, quiet_zone=1)
|
||||
|
||||
urlencoded_name = quote(doc.name)
|
||||
name = frappe.generate_hash(doc.name, 5)
|
||||
|
||||
# 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({
|
||||
"doctype": "File",
|
||||
"file_name": filename,
|
||||
|
||||
@@ -8,7 +8,6 @@ import frappe
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint
|
||||
from frappe.utils.nestedset import get_root_of
|
||||
|
||||
|
||||
class SellingSettings(Document):
|
||||
@@ -37,9 +36,3 @@ class SellingSettings(Document):
|
||||
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)
|
||||
|
||||
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')
|
||||
|
||||
@@ -304,7 +304,6 @@ def set_more_defaults():
|
||||
|
||||
def update_selling_defaults():
|
||||
selling_settings = frappe.get_doc("Selling Settings")
|
||||
selling_settings.set_default_customer_group_and_territory()
|
||||
selling_settings.cust_master_name = "Customer Name"
|
||||
selling_settings.so_required = "No"
|
||||
selling_settings.dn_required = "No"
|
||||
|
||||
@@ -53,6 +53,7 @@ def before_tests():
|
||||
|
||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
|
||||
enable_all_roles_and_domains()
|
||||
set_defaults_for_tests()
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
@@ -127,6 +128,14 @@ def enable_all_roles_and_domains():
|
||||
[d.name for d in domains])
|
||||
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):
|
||||
for r in records:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
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):
|
||||
'''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)
|
||||
|
||||
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)
|
||||
repost_current_voucher(args, allow_negative_stock, via_landed_cost_voucher)
|
||||
|
||||
def get_bin_details(bin_name):
|
||||
return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
|
||||
@@ -160,4 +138,4 @@ def update_qty(bin_name, args):
|
||||
'indented_qty': indented_qty,
|
||||
'planned_qty': planned_qty,
|
||||
'projected_qty': projected_qty
|
||||
})
|
||||
})
|
||||
|
||||
@@ -956,7 +956,7 @@
|
||||
"image_field": "image",
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-30 02:33:06.572442",
|
||||
"modified": "2021-12-03 08:32:03.869294",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
@@ -1023,7 +1023,7 @@
|
||||
"search_fields": "item_name,description,item_group,customer_code",
|
||||
"show_name_in_global_search": 1,
|
||||
"show_preview_popup": 1,
|
||||
"sort_field": "idx desc,modified desc",
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"title_field": "item_name",
|
||||
"track_changes": 1
|
||||
|
||||
@@ -496,7 +496,6 @@ class Item(Document):
|
||||
|
||||
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)
|
||||
|
||||
@@ -510,7 +509,6 @@ class Item(Document):
|
||||
repost_stock(new_name, warehouse)
|
||||
|
||||
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):
|
||||
if self.is_new():
|
||||
|
||||
@@ -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])
|
||||
|
||||
# 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)
|
||||
outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ def get_sle(**args):
|
||||
class TestStockEntry(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.set_user("Administrator")
|
||||
frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
|
||||
|
||||
def test_fifo(self):
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
|
||||
@@ -583,6 +584,65 @@ class TestStockEntry(unittest.TestCase):
|
||||
self.assertEqual(fg_cost,
|
||||
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):
|
||||
bom_no = frappe.db.get_value("BOM", {"item": "_Test Variant Item",
|
||||
|
||||
@@ -33,65 +33,6 @@ class TestWarehouse(ERPNextTestCase):
|
||||
self.assertEqual(p_warehouse.name, child_warehouse.parent_warehouse)
|
||||
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):
|
||||
company = "_Test Company"
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"creation": "2013-03-07 18:50:32",
|
||||
"description": "A logical Warehouse against which stock entries are made.",
|
||||
"doctype": "DocType",
|
||||
@@ -245,7 +244,7 @@
|
||||
"idx": 1,
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-09 19:54:56.263965",
|
||||
"modified": "2021-12-03 04:40:06.414630",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Warehouse",
|
||||
|
||||
@@ -10,7 +10,6 @@ from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.utils import cint, flt
|
||||
from frappe.utils.nestedset import NestedSet
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock import get_warehouse_account
|
||||
|
||||
|
||||
@@ -68,57 +67,6 @@ class Warehouse(NestedSet):
|
||||
return frappe.db.sql("""select name from `tabWarehouse`
|
||||
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):
|
||||
if self.is_group:
|
||||
self.convert_to_ledger()
|
||||
|
||||
@@ -7,10 +7,11 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
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
|
||||
|
||||
import erpnext
|
||||
from erpnext.stock.doctype.bin.bin import update_qty as update_bin_qty
|
||||
from erpnext.stock.utils import (
|
||||
get_incoming_outgoing_rate_for_cancel,
|
||||
get_or_make_bin,
|
||||
@@ -18,19 +19,15 @@ from erpnext.stock.utils import (
|
||||
)
|
||||
|
||||
|
||||
# future reposting
|
||||
class NegativeStockError(frappe.ValidationError): pass
|
||||
class SerialNoExistsInFutureTransaction(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
_exceptions = frappe.local('stockledger_exceptions')
|
||||
# _exceptions = []
|
||||
|
||||
def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False):
|
||||
from erpnext.controllers.stock_controller import future_sle_exists
|
||||
if sl_entries:
|
||||
from erpnext.stock.utils import update_bin
|
||||
|
||||
cancel = sl_entries[0].get("is_cancelled")
|
||||
if cancel:
|
||||
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
|
||||
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):
|
||||
return frappe._dict({
|
||||
@@ -795,10 +823,10 @@ class update_entries_after(object):
|
||||
|
||||
def update_bin(self):
|
||||
# update bin for each warehouse
|
||||
for warehouse, data in iteritems(self.data):
|
||||
bin_record = get_or_make_bin(self.item_code, warehouse)
|
||||
for warehouse, data in self.data.items():
|
||||
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,
|
||||
"actual_qty": data.qty_after_transaction,
|
||||
"stock_value": data.stock_value
|
||||
|
||||
@@ -188,7 +188,7 @@ def get_bin(item_code, warehouse):
|
||||
bin_obj.flags.ignore_permissions = True
|
||||
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})
|
||||
|
||||
if not bin_record:
|
||||
@@ -204,11 +204,12 @@ def get_or_make_bin(item_code, warehouse) -> str:
|
||||
return bin_record
|
||||
|
||||
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
|
||||
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
|
||||
if is_stock_item:
|
||||
bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
|
||||
update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
|
||||
bin_name = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
|
||||
update_stock(bin_name, 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")))
|
||||
|
||||
|
||||
Reference in New Issue
Block a user