Merge branch 'develop' into fix-adjust-asset-value

This commit is contained in:
Ganga Manoj 2021-12-08 19:20:23 +05:30 committed by GitHub
commit 51c707a0ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 883 additions and 403 deletions

View File

@ -374,12 +374,13 @@ def make_gl_entries(doc, credit_account, debit_account, against,
frappe.db.commit() frappe.db.commit()
except Exception as e: except Exception as e:
if frappe.flags.in_test: if frappe.flags.in_test:
traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
raise e raise e
else: else:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(message=traceback) frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process): def send_mail(deferred_process):
@ -446,10 +447,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
if submit: if submit:
journal_entry.submit() journal_entry.submit()
frappe.db.commit()
except Exception: except Exception:
frappe.db.rollback() frappe.db.rollback()
traceback = frappe.get_traceback() traceback = frappe.get_traceback()
frappe.log_error(message=traceback) frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True frappe.flags.deferred_accounting_error = True

View File

@ -68,10 +68,12 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
party_address, shipping_address if party_type != "Supplier" else party_address) party_address, shipping_address if party_type != "Supplier" else party_address)
if not party_details.get("taxes_and_charges"): tax_template = set_taxes(party.name, party_type, posting_date, company,
party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, billing_address=party_address, shipping_address=shipping_address)
billing_address=party_address, shipping_address=shipping_address)
if tax_template:
party_details['taxes_and_charges'] = tax_template
if cint(fetch_payment_terms_template): if cint(fetch_payment_terms_template):
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)

View File

@ -109,7 +109,11 @@ class ReceivablePayableReport(object):
invoiced = 0.0, invoiced = 0.0,
paid = 0.0, paid = 0.0,
credit_note = 0.0, credit_note = 0.0,
outstanding = 0.0 outstanding = 0.0,
invoiced_in_account_currency = 0.0,
paid_in_account_currency = 0.0,
credit_note_in_account_currency = 0.0,
outstanding_in_account_currency = 0.0
) )
self.get_invoices(gle) self.get_invoices(gle)
@ -150,21 +154,28 @@ class ReceivablePayableReport(object):
# gle_balance will be the total "debit - credit" for receivable type reports and # gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports # and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle) gle_balance = self.get_gle_balance(gle)
gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
if gle_balance > 0: if gle_balance > 0:
if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher:
# debit against sales / purchase invoice # debit against sales / purchase invoice
row.paid -= gle_balance row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
else: else:
# invoice # invoice
row.invoiced += gle_balance row.invoiced += gle_balance
row.invoiced_in_account_currency += gle_balance_in_account_currency
else: else:
# payment or credit note for receivables # payment or credit note for receivables
if self.is_invoice(gle): if self.is_invoice(gle):
# stand alone debit / credit note # stand alone debit / credit note
row.credit_note -= gle_balance row.credit_note -= gle_balance
row.credit_note_in_account_currency -= gle_balance_in_account_currency
else: else:
# advance / unlinked payment or other adjustment # advance / unlinked payment or other adjustment
row.paid -= gle_balance row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
if gle.cost_center: if gle.cost_center:
row.cost_center = str(gle.cost_center) row.cost_center = str(gle.cost_center)
@ -216,8 +227,13 @@ class ReceivablePayableReport(object):
# as we can use this to filter out invoices without outstanding # as we can use this to filter out invoices without outstanding
for key, row in self.voucher_balance.items(): for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \
row.credit_note_in_account_currency, self.currency_precision)
row.invoice_grand_total = row.invoiced row.invoice_grand_total = row.invoiced
if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \
(abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision):
# non-zero oustanding, we must consider this row # non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms: if self.is_invoice(row) and self.filters.based_on_payment_terms:
@ -583,12 +599,14 @@ class ReceivablePayableReport(object):
else: else:
select_fields = "debit, credit" select_fields = "debit, credit"
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
remarks = ", remarks" if self.filters.get("show_remarks") else "" remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql(""" self.gl_entries = frappe.db.sql("""
select select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, {0} {remarks} against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
from from
`tabGL Entry` `tabGL Entry`
where where
@ -596,8 +614,8 @@ class ReceivablePayableReport(object):
and is_cancelled = 0 and is_cancelled = 0
and party_type=%s and party_type=%s
and (party is not null and party != '') and (party is not null and party != '')
{1} {2} {3}""" {2} {3} {4}"""
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self): def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"): if self.filters.get("sales_person"):
@ -718,6 +736,13 @@ class ReceivablePayableReport(object):
# get the balance of the GL (debit - credit) or reverse balance based on report type # get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle) return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
def get_gle_balance_in_account_currency(self, gle):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle)
def get_reverse_balance_in_account_currency(self, gle):
return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency')
def get_reverse_balance(self, gle): def get_reverse_balance(self, gle):
# get "credit" balance if report type is "debit" and vice versa # get "credit" balance if report type is "debit" and vice versa
return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') return gle.get('debit' if self.dr_or_cr=='credit' else 'credit')

View File

@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', {
if (frm.doc.docstatus==1) { if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) { if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() { frm.add_custom_button(__("Transfer Asset"), function() {
erpnext.asset.transfer_asset(frm); erpnext.asset.transfer_asset(frm);
}, __("Manage")); }, __("Manage"));
frm.add_custom_button("Scrap Asset", function() { frm.add_custom_button(__("Scrap Asset"), function() {
erpnext.asset.scrap_asset(frm); erpnext.asset.scrap_asset(frm);
}, __("Manage")); }, __("Manage"));
frm.add_custom_button("Sell Asset", function() { frm.add_custom_button(__("Sell Asset"), function() {
frm.trigger("make_sales_invoice"); frm.trigger("make_sales_invoice");
}, __("Manage")); }, __("Manage"));
} else if (frm.doc.status=='Scrapped') { } else if (frm.doc.status=='Scrapped') {
frm.add_custom_button("Restore Asset", function() { frm.add_custom_button(__("Restore Asset"), function() {
erpnext.asset.restore_asset(frm); erpnext.asset.restore_asset(frm);
}, __("Manage")); }, __("Manage"));
} }
@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', {
} }
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) { if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
frm.add_custom_button("View General Ledger", function() { frm.add_custom_button(__("View General Ledger"), function() {
frappe.route_options = { frappe.route_options = {
"voucher_no": frm.doc.name, "voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date, "from_date": frm.doc.available_for_use_date,

View File

@ -192,8 +192,7 @@ class Asset(AccountsController):
# value_after_depreciation - current Asset value # value_after_depreciation - current Asset value
if self.docstatus == 1 and d.value_after_depreciation: if self.docstatus == 1 and d.value_after_depreciation:
value_after_depreciation = (flt(d.value_after_depreciation) - value_after_depreciation = flt(d.value_after_depreciation)
flt(self.opening_accumulated_depreciation))
else: else:
value_after_depreciation = (flt(self.gross_purchase_amount) - value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation)) flt(self.opening_accumulated_depreciation))
@ -241,7 +240,7 @@ class Asset(AccountsController):
break break
# For first row # For first row
if has_pro_rata and n==0: 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, depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date) self.available_for_use_date, d.depreciation_start_date)
@ -254,7 +253,7 @@ class Asset(AccountsController):
if not self.flags.increase_in_asset_life: 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 # 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, self.to_date = add_months(self.available_for_use_date,
n * 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_without_pro_rata = depreciation_amount
@ -354,7 +353,12 @@ class Asset(AccountsController):
# if it returns True, depreciation_amount will not be equal for the first and last rows # if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row): def check_is_pro_rata(self, row):
has_pro_rata = False has_pro_rata = False
days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
# if not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = self.get_modified_available_for_use_date(row)
days = date_diff(row.depreciation_start_date, from_date) + 1
# if frequency_of_depreciation is 12 months, total_days = 365 # if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation) total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
@ -364,6 +368,9 @@ class Asset(AccountsController):
return has_pro_rata return has_pro_rata
def get_modified_available_for_use_date(self, row):
return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation))
def validate_asset_finance_books(self, row): def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount): if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount") frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
@ -402,10 +409,11 @@ class Asset(AccountsController):
# to ensure that final accumulated depreciation amount is accurate # to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book): def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book):
depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book) if not self.opening_accumulated_depreciation:
depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata: if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row depreciation_amount_for_last_row = depreciation_amount_without_pro_rata - depreciation_amount_for_first_row
return depreciation_amount_for_last_row return depreciation_amount_for_last_row
@ -850,13 +858,11 @@ def get_total_days(date, frequency):
@erpnext.allow_regional @erpnext.allow_regional
def get_depreciation_amount(asset, depreciable_value, row): def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time # if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life: if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - depreciation_amount = (flt(asset.gross_purchase_amount) -
flt(row.expected_value_after_useful_life)) / depreciation_left flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair # if the Depreciation Schedule is being modified after Asset Repair
else: else:

View File

@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None):
je.finance_book = d.finance_book je.finance_book = d.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount) je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account)
credit_entry = { credit_entry = {
"account": accumulated_depreciation_account, "account": credit_account,
"credit_in_account_currency": d.depreciation_amount, "credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset", "reference_type": "Asset",
"reference_name": asset.name, "reference_name": asset.name,
@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None):
} }
debit_entry = { debit_entry = {
"account": depreciation_expense_account, "account": debit_account,
"debit_in_account_currency": d.depreciation_amount, "debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset", "reference_type": "Asset",
"reference_name": asset.name, "reference_name": asset.name,
@ -132,6 +134,20 @@ def get_depreciation_accounts(asset):
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
if root_type == "Expense":
credit_account = accumulated_depreciation_account
debit_account = depreciation_expense_account
elif root_type == "Income":
credit_account = depreciation_expense_account
debit_account = accumulated_depreciation_account
else:
frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account."))
return credit_account, debit_account
@frappe.whitelist() @frappe.whitelist()
def scrap_asset(asset_name): def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name) asset = frappe.get_doc("Asset", asset_name)

View File

@ -409,19 +409,18 @@ class TestDepreciationMethods(AssetSetup):
calculate_depreciation = 1, calculate_depreciation = 1,
available_for_use_date = "2030-06-06", available_for_use_date = "2030-06-06",
is_existing_asset = 1, is_existing_asset = 1,
number_of_depreciations_booked = 1, number_of_depreciations_booked = 2,
opening_accumulated_depreciation = 40000, opening_accumulated_depreciation = 47095.89,
expected_value_after_useful_life = 10000, expected_value_after_useful_life = 10000,
depreciation_start_date = "2030-12-31", depreciation_start_date = "2032-12-31",
total_number_of_depreciations = 3, total_number_of_depreciations = 3,
frequency_of_depreciation = 12 frequency_of_depreciation = 12
) )
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
expected_schedules = [ expected_schedules = [
["2030-12-31", 14246.58, 54246.58], ["2032-12-31", 30000.0, 77095.89],
["2031-12-31", 25000.00, 79246.58], ["2033-06-06", 12904.11, 90000.0]
["2032-06-06", 10753.42, 90000.00]
] ]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")] for d in asset.get("schedules")]
@ -869,6 +868,72 @@ class TestDepreciationBasics(AssetSetup):
self.assertFalse(asset.schedules[1].journal_entry) self.assertFalse(asset.schedules[1].journal_entry)
self.assertFalse(asset.schedules[2].journal_entry) self.assertFalse(asset.schedules[2].journal_entry)
def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self):
"""Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account."""
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = "2019-12-31",
depreciation_start_date = "2020-12-31",
frequency_of_depreciation = 12,
total_number_of_depreciations = 3,
expected_value_after_useful_life = 10000,
submit = 1
)
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
for entry in accounting_entries:
if entry["account"] == "_Test Depreciations - _TC":
self.assertTrue(entry["debit"])
self.assertFalse(entry["credit"])
else:
self.assertTrue(entry["credit"])
self.assertFalse(entry["debit"])
def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self):
"""Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account."""
depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
depr_expense_account.root_type = "Income"
depr_expense_account.parent_account = "Income - _TC"
depr_expense_account.save()
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = "2019-12-31",
depreciation_start_date = "2020-12-31",
frequency_of_depreciation = 12,
total_number_of_depreciations = 3,
expected_value_after_useful_life = 10000,
submit = 1
)
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
for entry in accounting_entries:
if entry["account"] == "_Test Depreciations - _TC":
self.assertTrue(entry["credit"])
self.assertFalse(entry["debit"])
else:
self.assertTrue(entry["debit"])
self.assertFalse(entry["credit"])
# resetting
depr_expense_account.root_type = "Expense"
depr_expense_account.parent_account = "Expenses - _TC"
depr_expense_account.save()
def test_clear_depreciation_schedule(self): def test_clear_depreciation_schedule(self):
"""Tests if clear_depreciation_schedule() works as expected.""" """Tests if clear_depreciation_schedule() works as expected."""

View File

@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', {
var d = locals[cdt][cdn]; var d = locals[cdt][cdn];
return { return {
"filters": { "filters": {
"root_type": "Expense", "root_type": ["in", ["Expense", "Income"]],
"is_group": 0, "is_group": 0,
"company": d.company_name "company": d.company_name
} }

View File

@ -42,10 +42,10 @@ class AssetCategory(Document):
def validate_account_types(self): def validate_account_types(self):
account_type_map = { account_type_map = {
'fixed_asset_account': { 'account_type': 'Fixed Asset' }, 'fixed_asset_account': {'account_type': ['Fixed Asset']},
'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' }, 'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']},
'depreciation_expense_account': { 'root_type': 'Expense' }, 'depreciation_expense_account': {'root_type': ['Expense', 'Income']},
'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' } 'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']}
} }
for d in self.accounts: for d in self.accounts:
for fieldname in account_type_map.keys(): for fieldname in account_type_map.keys():
@ -53,11 +53,11 @@ class AssetCategory(Document):
selected_account = d.get(fieldname) selected_account = d.get(fieldname)
key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match) selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
expected_key_type = account_type_map[fieldname][key_to_match] expected_key_types = account_type_map[fieldname][key_to_match]
if selected_key_type != expected_key_type: if selected_key_type not in expected_key_types:
frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), .format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)),
title=_("Invalid Account")) title=_("Invalid Account"))
def valide_cwip_account(self): def valide_cwip_account(self):

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('CRM Settings', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,114 @@
{
"actions": [],
"creation": "2021-09-09 17:03:22.754446",
"description": "Settings for Selling Module",
"doctype": "DocType",
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
"section_break_5",
"campaign_naming_by",
"allow_lead_duplication_based_on_emails",
"column_break_4",
"create_event_on_next_contact_date",
"auto_creation_of_contact",
"opportunity_section",
"close_opportunity_after_days",
"column_break_9",
"create_event_on_next_contact_date_opportunity",
"quotation_section",
"default_valid_till"
],
"fields": [
{
"fieldname": "campaign_naming_by",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Campaign Naming By",
"options": "Campaign Name\nNaming Series"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "default_valid_till",
"fieldtype": "Data",
"label": "Default Quotation Validity Days"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Lead"
},
{
"default": "0",
"fieldname": "allow_lead_duplication_based_on_emails",
"fieldtype": "Check",
"label": "Allow Lead Duplication based on Emails"
},
{
"default": "1",
"fieldname": "auto_creation_of_contact",
"fieldtype": "Check",
"label": "Auto Creation of Contact"
},
{
"default": "1",
"fieldname": "create_event_on_next_contact_date",
"fieldtype": "Check",
"label": "Create Event on Next Contact Date"
},
{
"fieldname": "opportunity_section",
"fieldtype": "Section Break",
"label": "Opportunity"
},
{
"default": "15",
"description": "Auto close Opportunity Replied after the no. of days mentioned above",
"fieldname": "close_opportunity_after_days",
"fieldtype": "Int",
"label": "Close Replied Opportunity After Days"
},
{
"default": "1",
"fieldname": "create_event_on_next_contact_date_opportunity",
"fieldtype": "Check",
"label": "Create Event on Next Contact Date"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "quotation_section",
"fieldtype": "Section Break",
"label": "Quotation"
}
],
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
"modified": "2021-11-03 09:00:36.883496",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class CRMSettings(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestCRMSettings(unittest.TestCase):
pass

View File

@ -11,6 +11,7 @@ from frappe.utils import (
cint, cint,
comma_and, comma_and,
cstr, cstr,
get_link_to_form,
getdate, getdate,
has_gravatar, has_gravatar,
nowdate, nowdate,
@ -91,13 +92,14 @@ class Lead(SellingController):
self.contact_doc.save() self.contact_doc.save()
def add_calendar_event(self, opts=None, force=False): def add_calendar_event(self, opts=None, force=False):
super(Lead, self).add_calendar_event({ if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'):
"owner": self.lead_owner, super(Lead, self).add_calendar_event({
"starts_on": self.contact_date, "owner": self.lead_owner,
"ends_on": self.ends_on or "", "starts_on": self.contact_date,
"subject": ('Contact ' + cstr(self.lead_name)), "ends_on": self.ends_on or "",
"description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '') "subject": ('Contact ' + cstr(self.lead_name)),
}, force) "description": ('Contact ' + cstr(self.lead_name)) + (self.contact_by and ('. By : ' + cstr(self.contact_by)) or '')
}, force)
def update_prospects(self): def update_prospects(self):
prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent']) prospects = frappe.get_all('Prospect Lead', filters={'lead': self.name}, fields=['parent'])
@ -108,12 +110,13 @@ class Lead(SellingController):
def check_email_id_is_unique(self): def check_email_id_is_unique(self):
if self.email_id: if self.email_id:
# validate email is unique # validate email is unique
duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]}) if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'):
duplicate_leads = [lead.name for lead in duplicate_leads] duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads]
if duplicate_leads: if duplicate_leads:
frappe.throw(_("Email Address must be unique, already exists for {0}") frappe.throw(_("Email Address must be unique, already exists for {0}")
.format(comma_and(duplicate_leads)), frappe.DuplicateEntryError) .format(comma_and(duplicate_leads)), frappe.DuplicateEntryError)
def on_trash(self): def on_trash(self):
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
@ -172,41 +175,42 @@ class Lead(SellingController):
self.title = self.company_name or self.lead_name self.title = self.company_name or self.lead_name
def create_contact(self): def create_contact(self):
if not self.lead_name: if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'):
self.set_full_name() if not self.lead_name:
self.set_lead_name() self.set_full_name()
self.set_lead_name()
contact = frappe.new_doc("Contact") contact = frappe.new_doc("Contact")
contact.update({ contact.update({
"first_name": self.first_name or self.lead_name, "first_name": self.first_name or self.lead_name,
"last_name": self.last_name, "last_name": self.last_name,
"salutation": self.salutation, "salutation": self.salutation,
"gender": self.gender, "gender": self.gender,
"designation": self.designation, "designation": self.designation,
"company_name": self.company_name, "company_name": self.company_name,
})
if self.email_id:
contact.append("email_ids", {
"email_id": self.email_id,
"is_primary": 1
}) })
if self.phone: if self.email_id:
contact.append("phone_nos", { contact.append("email_ids", {
"phone": self.phone, "email_id": self.email_id,
"is_primary_phone": 1 "is_primary": 1
}) })
if self.mobile_no: if self.phone:
contact.append("phone_nos", { contact.append("phone_nos", {
"phone": self.mobile_no, "phone": self.phone,
"is_primary_mobile_no":1 "is_primary_phone": 1
}) })
contact.insert(ignore_permissions=True) if self.mobile_no:
contact.append("phone_nos", {
"phone": self.mobile_no,
"is_primary_mobile_no":1
})
return contact contact.insert(ignore_permissions=True)
return contact
@frappe.whitelist() @frappe.whitelist()
def make_customer(source_name, target_doc=None): def make_customer(source_name, target_doc=None):

View File

@ -8,6 +8,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.email.inbox import link_communication_to_document from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType
from frappe.utils import cint, cstr, flt, get_fullname from frappe.utils import cint, cstr, flt, get_fullname
from erpnext.setup.utils import get_exchange_rate from erpnext.setup.utils import get_exchange_rate
@ -28,7 +29,6 @@ class Opportunity(TransactionBase):
}) })
self.make_new_lead_if_required() self.make_new_lead_if_required()
self.validate_item_details() self.validate_item_details()
self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name() self.validate_cust_name()
@ -70,21 +70,21 @@ class Opportunity(TransactionBase):
"""Set lead against new opportunity""" """Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email: if (not self.get("party_name")) and self.contact_email:
# check if customer is already created agains the self.contact_email # check if customer is already created agains the self.contact_email
customer = frappe.db.sql("""select dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact")
distinct `tabDynamic Link`.link_name as customer customer = frappe.qb.from_(
from dynamic_link
`tabContact`, ).join(
`tabDynamic Link` contact
where `tabContact`.email_id='{0}' ).on(
and (contact.name == dynamic_link.parent)
`tabContact`.name=`tabDynamic Link`.parent & (dynamic_link.link_doctype == "Customer")
and & (contact.email_id == self.contact_email)
ifnull(`tabDynamic Link`.link_name, '')<>'' ).select(
and dynamic_link.link_name
`tabDynamic Link`.link_doctype='Customer' ).distinct().run(as_dict=True)
""".format(self.contact_email), as_dict=True)
if customer and customer[0].customer: if customer and customer[0].link_name:
self.party_name = customer[0].customer self.party_name = customer[0].link_name
self.opportunity_from = "Customer" self.opportunity_from = "Customer"
return return
@ -191,30 +191,31 @@ class Opportunity(TransactionBase):
self.add_calendar_event() self.add_calendar_event()
def add_calendar_event(self, opts=None, force=False): def add_calendar_event(self, opts=None, force=False):
if not opts: if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'):
opts = frappe._dict() if not opts:
opts = frappe._dict()
opts.description = "" opts.description = ""
opts.contact_date = self.contact_date opts.contact_date = self.contact_date
if self.party_name and self.opportunity_from == 'Customer': if self.party_name and self.opportunity_from == 'Customer':
if self.contact_person: if self.contact_person:
opts.description = 'Contact '+cstr(self.contact_person) opts.description = 'Contact '+cstr(self.contact_person)
else: else:
opts.description = 'Contact customer '+cstr(self.party_name) opts.description = 'Contact customer '+cstr(self.party_name)
elif self.party_name and self.opportunity_from == 'Lead': elif self.party_name and self.opportunity_from == 'Lead':
if self.contact_display: if self.contact_display:
opts.description = 'Contact '+cstr(self.contact_display) opts.description = 'Contact '+cstr(self.contact_display)
else: else:
opts.description = 'Contact lead '+cstr(self.party_name) opts.description = 'Contact lead '+cstr(self.party_name)
opts.subject = opts.description opts.subject = opts.description
opts.description += '. By : ' + cstr(self.contact_by) opts.description += '. By : ' + cstr(self.contact_by)
if self.to_discuss: if self.to_discuss:
opts.description += ' To Discuss : ' + cstr(self.to_discuss) opts.description += ' To Discuss : ' + cstr(self.to_discuss)
super(Opportunity, self).add_calendar_event(opts, force) super(Opportunity, self).add_calendar_event(opts, force)
def validate_item_details(self): def validate_item_details(self):
if not self.get('items'): if not self.get('items'):
@ -363,7 +364,7 @@ def set_multiple_status(names, status):
def auto_close_opportunity(): def auto_close_opportunity():
""" auto close the `Replied` Opportunities after 7 days """ """ auto close the `Replied` Opportunities after 7 days """
auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15 auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15
opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and
modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """, (auto_close_after_days), as_dict=True) modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """, (auto_close_after_days), as_dict=True)

View File

@ -20,7 +20,6 @@
"configuration_cb", "configuration_cb",
"shipping_account_head", "shipping_account_head",
"section_break_12", "section_break_12",
"nexus_address",
"nexus" "nexus"
], ],
"fields": [ "fields": [
@ -87,15 +86,11 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "nexus",
"fieldname": "section_break_12", "fieldname": "section_break_12",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Nexus List" "label": "Nexus List"
}, },
{
"fieldname": "nexus_address",
"fieldtype": "HTML",
"label": "Nexus Address"
},
{ {
"fieldname": "nexus", "fieldname": "nexus",
"fieldtype": "Table", "fieldtype": "Table",
@ -107,20 +102,21 @@
"fieldname": "configuration_cb", "fieldname": "configuration_cb",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "Company" "options": "Company"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
} }
], ],
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-11-08 18:02:29.232090", "migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
"modified": "2021-11-30 11:17:24.647979",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "TaxJar Settings", "name": "TaxJar Settings",

View File

@ -16,9 +16,9 @@ from erpnext.erpnext_integrations.taxjar_integration import get_client
class TaxJarSettings(Document): class TaxJarSettings(Document):
def on_update(self): def on_update(self):
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions") TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax") TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax
TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox") TAXJAR_SANDBOX_MODE = self.is_sandbox
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'}) fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden') fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')

View File

@ -323,10 +323,14 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
target.maintenance_schedule = source.name target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id target.maintenance_schedule_detail = s_id
def update_sales(source, target, parent): def update_sales_and_serial(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person') sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person target.service_person = sales_person
target.serial_no = '' serial_nos = get_serial_nos(target.serial_no)
if len(serial_nos) == 1:
target.serial_no = serial_nos[0]
else:
target.serial_no = ''
doclist = get_mapped_doc("Maintenance Schedule", source_name, { doclist = get_mapped_doc("Maintenance Schedule", source_name, {
"Maintenance Schedule": { "Maintenance Schedule": {
@ -342,7 +346,7 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"Maintenance Schedule Item": { "Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose", "doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name, "condition": lambda doc: doc.item_name == item_name,
"postprocess": update_sales "postprocess": update_sales_and_serial
} }
}, target_doc) }, target_doc)

View File

@ -43,14 +43,11 @@ frappe.ui.form.on('Maintenance Visit', {
} }
}); });
} }
else {
frm.clear_table("purposes");
}
if (!frm.doc.status) { if (!frm.doc.status) {
frm.set_value({ status: 'Draft' }); frm.set_value({ status: 'Draft' });
} }
if (frm.doc.__islocal) { if (frm.doc.__islocal) {
frm.clear_table("purposes");
frm.set_value({ mntc_date: frappe.datetime.get_today() }); frm.set_value({ mntc_date: frappe.datetime.get_today() });
} }
}, },

View File

@ -278,6 +278,7 @@ erpnext.patches.v13_0.update_tds_check_field #3
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.update_recipient_email_digest 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.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
erpnext.patches.v13_0.einvoicing_deprecation_warning erpnext.patches.v13_0.einvoicing_deprecation_warning
@ -312,3 +313,4 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_pan_field_for_india #2 erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v14_0.delete_hub_doctypes erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.create_ksa_vat_custom_fields erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v14_0.migrate_crm_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,15 @@
import frappe
from frappe import _
def execute():
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

@ -0,0 +1,16 @@
import frappe
def execute():
settings = frappe.db.get_value('Selling Settings', 'Selling Settings', [
'campaign_naming_by',
'close_opportunity_after_days',
'default_valid_till'
], as_dict=True)
frappe.reload_doc('crm', 'doctype', 'crm_settings')
frappe.db.set_value('CRM Settings', 'CRM Settings', {
'campaign_naming_by': settings.campaign_naming_by,
'close_opportunity_after_days': settings.close_opportunity_after_days,
'default_valid_till': settings.default_valid_till
})

View File

@ -1106,7 +1106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
$.each(this.frm.doc.taxes || [], function(i, d) { $.each(this.frm.doc.taxes || [], function(i, d) {
if(d.charge_type == "Actual") { if(d.charge_type == "Actual") {
frappe.model.set_value(d.doctype, d.name, "tax_amount", frappe.model.set_value(d.doctype, d.name, "tax_amount",
flt(d.tax_amount) / flt(exchange_rate)); flt(d.base_tax_amount) / flt(exchange_rate));
} }
}); });
} }

View File

@ -82,7 +82,6 @@ class TaxExemption80GCertificate(Document):
memberships = frappe.db.get_all('Membership', { memberships = frappe.db.get_all('Membership', {
'member': self.member, 'member': self.member,
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)], 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled') 'membership_status': ('!=', 'Cancelled')
}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date') }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')

View File

@ -206,26 +206,17 @@ def get_regional_address_details(party_details, doctype, company):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template" master_doctype = "Sales Taxes and Charges Template"
get_tax_template_based_on_category(master_doctype, company, party_details) tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'):
return party_details
if not party_details.company_gstin:
return party_details
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template" master_doctype = "Purchase Taxes and Charges Template"
get_tax_template_based_on_category(master_doctype, company, party_details) tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'): if tax_template_by_category:
return party_details party_details['taxes_and_charges'] = tax_template_by_category
return
if not party_details.supplier_gstin:
return party_details
if not party_details.place_of_supply: return party_details if not party_details.place_of_supply: return party_details
if not party_details.company_gstin: return party_details if not party_details.company_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
@ -237,6 +228,7 @@ def get_regional_address_details(party_details, doctype, company):
if not default_tax: if not default_tax:
return party_details return party_details
party_details["taxes_and_charges"] = default_tax party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
@ -268,9 +260,7 @@ def get_tax_template_based_on_category(master_doctype, company, party_details):
default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')},
'name') 'name')
if default_tax: return default_tax
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
def get_tax_template(master_doctype, company, is_inter_state, state_code): def get_tax_template(master_doctype, company, is_inter_state, state_code):
tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
@ -847,13 +837,11 @@ def update_taxable_values(doc, method):
doc.get('items')[item_count - 1].taxable_value += diff doc.get('items')[item_count - 1].taxable_value += diff
def get_depreciation_amount(asset, depreciable_value, row): def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"): if row.depreciation_method in ("Straight Line", "Manual"):
# if the Depreciation Schedule is being prepared for the first time # if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life: if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) - depreciation_amount = (flt(asset.gross_purchase_amount) -
flt(row.expected_value_after_useful_life)) / depreciation_left flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair # if the Depreciation Schedule is being modified after Asset Repair
else: else:

View File

@ -77,7 +77,7 @@ def create_qr_code(doc, method):
tlv_array.append(''.join([tag, length, value])) tlv_array.append(''.join([tag, length, value]))
# Invoice Amount # Invoice Amount
invoice_amount = str(doc.total) invoice_amount = str(doc.grand_total)
tag = bytes([4]).hex() tag = bytes([4]).hex()
length = bytes([len(invoice_amount)]).hex() length = bytes([len(invoice_amount)]).hex()
value = invoice_amount.encode('utf-8').hex() value = invoice_amount.encode('utf-8').hex()
@ -101,8 +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)
name = frappe.generate_hash(doc.name, 5)
# making file # making file
filename = f"QR-CODE-{doc.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

@ -302,6 +302,109 @@ class TestQuotation(unittest.TestCase):
enable_calculate_bundle_price(enable=0) enable_calculate_bundle_price(enable=0)
def test_product_bundle_price_calculation_for_multiple_product_bundles_when_calculate_bundle_price_is_checked(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import make_item
make_item("_Test Product Bundle 1", {"is_stock_item": 0})
make_item("_Test Product Bundle 2", {"is_stock_item": 0})
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
make_item("_Test Bundle Item 3", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle 1",
["_Test Bundle Item 1", "_Test Bundle Item 2"])
make_product_bundle("_Test Product Bundle 2",
["_Test Bundle Item 2", "_Test Bundle Item 3"])
enable_calculate_bundle_price()
item_list = [
{
"item_code": "_Test Product Bundle 1",
"warehouse": "",
"qty": 1,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
},
{
"item_code": "_Test Product Bundle 2",
"warehouse": "",
"qty": 1,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
}
]
quotation = make_quotation(item_list=item_list, do_not_submit=1)
quotation.packed_items[0].rate = 100
quotation.packed_items[1].rate = 200
quotation.packed_items[2].rate = 200
quotation.packed_items[3].rate = 300
quotation.save()
expected_values = [300, 500]
for item in quotation.items:
self.assertEqual(item.amount, expected_values[item.idx-1])
enable_calculate_bundle_price(enable=0)
def test_packed_items_indices_are_reset_when_product_bundle_is_deleted_from_items_table(self):
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
from erpnext.stock.doctype.item.test_item import make_item
make_item("_Test Product Bundle 1", {"is_stock_item": 0})
make_item("_Test Product Bundle 2", {"is_stock_item": 0})
make_item("_Test Product Bundle 3", {"is_stock_item": 0})
make_item("_Test Bundle Item 1", {"is_stock_item": 1})
make_item("_Test Bundle Item 2", {"is_stock_item": 1})
make_item("_Test Bundle Item 3", {"is_stock_item": 1})
make_product_bundle("_Test Product Bundle 1",
["_Test Bundle Item 1", "_Test Bundle Item 2"])
make_product_bundle("_Test Product Bundle 2",
["_Test Bundle Item 2", "_Test Bundle Item 3"])
make_product_bundle("_Test Product Bundle 3",
["_Test Bundle Item 3", "_Test Bundle Item 1"])
item_list = [
{
"item_code": "_Test Product Bundle 1",
"warehouse": "",
"qty": 1,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
},
{
"item_code": "_Test Product Bundle 2",
"warehouse": "",
"qty": 1,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
},
{
"item_code": "_Test Product Bundle 3",
"warehouse": "",
"qty": 1,
"rate": 400,
"delivered_by_supplier": 1,
"supplier": '_Test Supplier'
}
]
quotation = make_quotation(item_list=item_list, do_not_submit=1)
del quotation.items[1]
quotation.save()
for id, item in enumerate(quotation.packed_items):
expected_index = id + 1
self.assertEqual(item.idx, expected_index)
test_records = frappe.get_test_records('Quotation') test_records = frappe.get_test_records('Quotation')
def enable_calculate_bundle_price(enable=1): def enable_calculate_bundle_price(enable=1):

View File

@ -925,6 +925,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None):
"supplier", "supplier",
"pricing_rules" "pricing_rules"
], ],
"condition": lambda doc: doc.parent_item in items_to_map
} }
}, target_doc, set_missing_values) }, target_doc, set_missing_values)
@ -977,6 +978,7 @@ def make_work_orders(items, sales_order, company, project=None):
description=i['description'] description=i['description']
)).insert() )).insert()
work_order.set_work_order_operations() work_order.set_work_order_operations()
work_order.flags.ignore_mandatory = True
work_order.save() work_order.save()
out.append(work_order) out.append(work_order)

View File

@ -11,11 +11,6 @@
"customer_group", "customer_group",
"column_break_4", "column_break_4",
"territory", "territory",
"crm_settings_section",
"campaign_naming_by",
"default_valid_till",
"column_break_9",
"close_opportunity_after_days",
"item_price_settings_section", "item_price_settings_section",
"selling_price_list", "selling_price_list",
"maintain_same_rate_action", "maintain_same_rate_action",
@ -43,13 +38,6 @@
"label": "Customer Naming By", "label": "Customer Naming By",
"options": "Customer Name\nNaming Series\nAuto Name" "options": "Customer Name\nNaming Series\nAuto Name"
}, },
{
"fieldname": "campaign_naming_by",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Campaign Naming By",
"options": "Campaign Name\nNaming Series\nAuto Name"
},
{ {
"fieldname": "customer_group", "fieldname": "customer_group",
"fieldtype": "Link", "fieldtype": "Link",
@ -71,18 +59,6 @@
"label": "Default Price List", "label": "Default Price List",
"options": "Price List" "options": "Price List"
}, },
{
"default": "15",
"description": "Auto close Opportunity after the no. of days mentioned above",
"fieldname": "close_opportunity_after_days",
"fieldtype": "Int",
"label": "Close Opportunity After Days"
},
{
"fieldname": "default_valid_till",
"fieldtype": "Data",
"label": "Default Quotation Validity Days"
},
{ {
"fieldname": "column_break_5", "fieldname": "column_break_5",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -169,15 +145,6 @@
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "crm_settings_section",
"fieldtype": "Section Break",
"label": "CRM Settings"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{ {
"fieldname": "item_price_settings_section", "fieldname": "item_price_settings_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
@ -204,7 +171,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-09-08 19:38:10.175989", "modified": "2021-09-13 12:32:17.004404",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",

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

@ -303,7 +303,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

@ -10,7 +10,7 @@
"idx": 0, "idx": 0,
"label": "ERPNext Settings", "label": "ERPNext Settings",
"links": [], "links": [],
"modified": "2021-10-26 21:32:55.323591", "modified": "2021-11-05 21:32:55.323591",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "ERPNext Settings", "name": "ERPNext Settings",
@ -123,6 +123,13 @@
"label": "Products Settings", "label": "Products Settings",
"link_to": "Products Settings", "link_to": "Products Settings",
"type": "DocType" "type": "DocType"
},
{
"doc_view": "",
"icon": "crm",
"label": "CRM Settings",
"link_to": "CRM Settings",
"type": "DocType"
} }
], ],
"title": "ERPNext Settings" "title": "ERPNext Settings"

View File

@ -22,7 +22,7 @@ def boot_session(bootinfo):
'customer_group') 'customer_group')
bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings', bootinfo.sysdefaults.allow_stale = cint(frappe.db.get_single_value('Accounts Settings',
'allow_stale')) 'allow_stale'))
bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('Selling Settings', bootinfo.sysdefaults.quotation_valid_till = cint(frappe.db.get_single_value('CRM Settings',
'default_valid_till')) 'default_valid_till'))
# if no company, show a dialog box to create a new company # if no company, show a dialog box to create a new company

View File

@ -6,7 +6,7 @@ import frappe
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Case from frappe.query_builder import Case
from frappe.query_builder.functions import Coalesce, Sum from frappe.query_builder.functions import Coalesce, Sum
from frappe.utils import flt, nowdate from frappe.utils import flt
class Bin(Document): class Bin(Document):
@ -127,33 +127,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

@ -1035,7 +1035,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",
@ -1103,7 +1103,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

@ -724,7 +724,6 @@ class Item(WebsiteGenerator):
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)
@ -738,7 +737,6 @@ class Item(WebsiteGenerator):
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
@frappe.whitelist() @frappe.whitelist()
def copy_specification_from_item_group(self): def copy_specification_from_item_group(self):

View File

@ -534,8 +534,6 @@ class TestItem(ERPNextTestCase):
def test_index_creation(self): def test_index_creation(self):
"check if index is getting created in db" "check if index is getting created in db"
from erpnext.stock.doctype.item.item import on_doctype_update
on_doctype_update()
indices = frappe.db.sql("show index from tabItem", as_dict=1) indices = frappe.db.sql("show index from tabItem", as_dict=1)
expected_columns = {"item_code", "item_name", "item_group", "route"} expected_columns = {"item_code", "item_name", "item_group", "route"}

View File

@ -108,9 +108,32 @@ def cleanup_packing_list(doc, parent_items):
packed_items = doc.get("packed_items") packed_items = doc.get("packed_items")
doc.set("packed_items", []) doc.set("packed_items", [])
for d in packed_items: for d in packed_items:
if d not in delete_list: if d not in delete_list:
doc.append("packed_items", d) add_item_to_packing_list(doc, d)
def add_item_to_packing_list(doc, packed_item):
doc.append("packed_items", {
'parent_item': packed_item.parent_item,
'item_code': packed_item.item_code,
'item_name': packed_item.item_name,
'uom': packed_item.uom,
'qty': packed_item.qty,
'rate': packed_item.rate,
'conversion_factor': packed_item.conversion_factor,
'description': packed_item.description,
'warehouse': packed_item.warehouse,
'batch_no': packed_item.batch_no,
'actual_batch_qty': packed_item.actual_batch_qty,
'serial_no': packed_item.serial_no,
'target_warehouse': packed_item.target_warehouse,
'actual_qty': packed_item.actual_qty,
'projected_qty': packed_item.projected_qty,
'incoming_rate': packed_item.incoming_rate,
'prevdoc_doctype': packed_item.prevdoc_doctype,
'parent_detail_docname': packed_item.parent_detail_docname
})
def update_product_bundle_price(doc, parent_items): def update_product_bundle_price(doc, parent_items):
"""Updates the prices of Product Bundles based on the rates of the Items in the bundle.""" """Updates the prices of Product Bundles based on the rates of the Items in the bundle."""
@ -128,7 +151,8 @@ def update_product_bundle_price(doc, parent_items):
else: else:
update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price) update_parent_item_price(doc, parent_items[parent_items_index][0], bundle_price)
bundle_price = 0 bundle_item_rate = bundle_item.rate if bundle_item.rate else 0
bundle_price = bundle_item.qty * bundle_item_rate
parent_items_index += 1 parent_items_index += 1
# for the last product bundle # for the last product bundle

View File

@ -54,9 +54,11 @@ class RepostItemValuation(Document):
@frappe.whitelist() @frappe.whitelist()
def restart_reposting(self): def restart_reposting(self):
self.set_status('Queued') self.set_status('Queued', write=False)
frappe.enqueue(repost, timeout=1800, queue='long', self.current_index = 0
job_name='repost_sle', now=True, doc=self) self.distinct_item_and_warehouse = None
self.items_to_be_repost = None
self.db_update()
def deduplicate_similar_repost(self): def deduplicate_similar_repost(self):
""" Deduplicate similar reposts based on item-warehouse-posting combination.""" """ Deduplicate similar reposts based on item-warehouse-posting combination."""

View File

@ -545,7 +545,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

@ -24,7 +24,8 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation, create_stock_reconciliation,
) )
from erpnext.stock.stock_ledger import get_previous_sle from erpnext.stock.stock_ledger import NegativeStockError, get_previous_sle
from erpnext.tests.utils import ERPNextTestCase, change_settings
def get_sle(**args): def get_sle(**args):
@ -38,9 +39,10 @@ def get_sle(**args):
order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition, order by timestamp(posting_date, posting_time) desc, creation desc limit 1"""% condition,
values, as_dict=1) values, as_dict=1)
class TestStockEntry(unittest.TestCase): class TestStockEntry(ERPNextTestCase):
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)
@ -582,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",
@ -868,6 +929,83 @@ class TestStockEntry(unittest.TestCase):
distributed_costs = [d.additional_cost for d in se.items] distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([40.0, 60.0], distributed_costs) self.assertEqual([40.0, 60.0], distributed_costs)
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle(self):
# Initialize item, batch, warehouse, opening qty
item_code = '_Test Future Neg Item'
batch_no = '_Test Future Neg Batch'
warehouses = [
'_Test Future Neg Warehouse Source',
'_Test Future Neg Warehouse Destination'
]
warehouse_names = initialize_records_for_future_negative_sle_test(
item_code, batch_no, warehouses,
opening_qty=2, posting_date='2021-07-01'
)
# Executing an illegal sequence should raise an error
sequence_of_entries = [
dict(item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
posting_date='2021-07-03',
purpose='Material Transfer'),
dict(item_code=item_code,
qty=2,
from_warehouse=warehouse_names[1],
to_warehouse=warehouse_names[0],
batch_no=batch_no,
posting_date='2021-07-04',
purpose='Material Transfer'),
dict(item_code=item_code,
qty=2,
from_warehouse=warehouse_names[0],
to_warehouse=warehouse_names[1],
batch_no=batch_no,
posting_date='2021-07-02', # Illegal SE
purpose='Material Transfer')
]
self.assertRaises(NegativeStockError, create_stock_entries, sequence_of_entries)
@change_settings("Stock Settings", {"allow_negative_stock": 0})
def test_future_negative_sle_batch(self):
from erpnext.stock.doctype.batch.test_batch import TestBatch
# Initialize item, batch, warehouse, opening qty
item_code = '_Test MultiBatch Item'
TestBatch.make_batch_item(item_code)
batch_nos = [] # store generate batches
warehouse = '_Test Warehouse - _TC'
se1 = make_stock_entry(
item_code=item_code,
qty=2,
to_warehouse=warehouse,
posting_date='2021-09-01',
purpose='Material Receipt'
)
batch_nos.append(se1.items[0].batch_no)
se2 = make_stock_entry(
item_code=item_code,
qty=2,
to_warehouse=warehouse,
posting_date='2021-09-03',
purpose='Material Receipt'
)
batch_nos.append(se2.items[0].batch_no)
with self.assertRaises(NegativeStockError) as nse:
make_stock_entry(item_code=item_code,
qty=1,
from_warehouse=warehouse,
batch_no=batch_nos[1],
posting_date='2021-09-02', # backdated consumption of 2nd batch
purpose='Material Issue')
def make_serialized_item(**args): def make_serialized_item(**args):
args = frappe._dict(args) args = frappe._dict(args)
se = frappe.copy_doc(test_records[0]) se = frappe.copy_doc(test_records[0])
@ -938,3 +1076,31 @@ def get_multiple_items():
] ]
test_records = frappe.get_test_records('Stock Entry') test_records = frappe.get_test_records('Stock Entry')
def initialize_records_for_future_negative_sle_test(
item_code, batch_no, warehouses, opening_qty, posting_date):
from erpnext.stock.doctype.batch.test_batch import TestBatch, make_new_batch
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
TestBatch.make_batch_item(item_code)
make_new_batch(item_code=item_code, batch_id=batch_no)
warehouse_names = [create_warehouse(w) for w in warehouses]
create_stock_reconciliation(
purpose='Opening Stock',
posting_date=posting_date,
posting_time='20:00:20',
item_code=item_code,
warehouse=warehouse_names[0],
valuation_rate=100,
qty=opening_qty,
batch_no=batch_no,
)
return warehouse_names
def create_stock_entries(sequence_of_entries):
for entry_detail in sequence_of_entries:
make_stock_entry(**entry_detail)

View File

@ -8,7 +8,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.core.doctype.role.role import get_users from frappe.core.doctype.role.role import get_users
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_days, cint, flt, formatdate, get_datetime, getdate from frappe.utils import add_days, cint, formatdate, get_datetime, getdate
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
@ -43,7 +43,6 @@ class StockLedgerEntry(Document):
def on_submit(self): def on_submit(self):
self.check_stock_frozen_date() self.check_stock_frozen_date()
self.actual_amt_check()
self.calculate_batch_qty() self.calculate_batch_qty()
if not self.get("via_landed_cost_voucher"): if not self.get("via_landed_cost_voucher"):
@ -57,18 +56,6 @@ class StockLedgerEntry(Document):
"sum(actual_qty)") or 0 "sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty) frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
def actual_amt_check(self):
"""Validate that qty at warehouse for selected batch is >=0"""
if self.batch_no and not self.get("allow_negative_stock"):
batch_bal_after_transaction = flt(frappe.db.sql("""select sum(actual_qty)
from `tabStock Ledger Entry`
where is_cancelled =0 and warehouse=%s and item_code=%s and batch_no=%s""",
(self.warehouse, self.item_code, self.batch_no))[0][0])
if batch_bal_after_transaction < 0:
frappe.throw(_("Stock balance in Batch {0} will become negative {1} for Item {2} at Warehouse {3}")
.format(self.batch_no, batch_bal_after_transaction, self.item_code, self.warehouse))
def validate_mandatory(self): def validate_mandatory(self):
mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company'] mandatory = ['warehouse','posting_date','voucher_type','voucher_no','company']
for k in mandatory: for k in mandatory:

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,9 +7,10 @@ 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
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,
@ -17,19 +18,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)
@ -64,7 +61,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({
@ -803,9 +831,9 @@ 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 self.data.items(): 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
@ -1061,17 +1089,36 @@ def validate_negative_qty_in_future_sle(args, allow_negative_stock=False):
allow_negative_stock = cint(allow_negative_stock) \ allow_negative_stock = cint(allow_negative_stock) \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock")) or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
if (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation") and not allow_negative_stock: if allow_negative_stock:
sle = get_future_sle_with_negative_qty(args) return
if sle: if not (args.actual_qty < 0 or args.voucher_type == "Stock Reconciliation"):
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format( return
abs(sle[0]["qty_after_transaction"]),
frappe.get_desk_link('Item', args.item_code), neg_sle = get_future_sle_with_negative_qty(args)
frappe.get_desk_link('Warehouse', args.warehouse), if neg_sle:
sle[0]["posting_date"], sle[0]["posting_time"], message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"])) abs(neg_sle[0]["qty_after_transaction"]),
frappe.get_desk_link('Item', args.item_code),
frappe.get_desk_link('Warehouse', args.warehouse),
neg_sle[0]["posting_date"], neg_sle[0]["posting_time"],
frappe.get_desk_link(neg_sle[0]["voucher_type"], neg_sle[0]["voucher_no"]))
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
if not args.batch_no:
return
neg_batch_sle = get_future_sle_with_negative_batch_qty(args)
if neg_batch_sle:
message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
abs(neg_batch_sle[0]["cumulative_total"]),
frappe.get_desk_link('Batch', args.batch_no),
frappe.get_desk_link('Warehouse', args.warehouse),
neg_batch_sle[0]["posting_date"], neg_batch_sle[0]["posting_time"],
frappe.get_desk_link(neg_batch_sle[0]["voucher_type"], neg_batch_sle[0]["voucher_no"]))
frappe.throw(message, NegativeStockError, title="Insufficient Stock for Batch")
frappe.throw(message, NegativeStockError, title='Insufficient Stock')
def get_future_sle_with_negative_qty(args): def get_future_sle_with_negative_qty(args):
return frappe.db.sql(""" return frappe.db.sql("""
@ -1090,6 +1137,29 @@ def get_future_sle_with_negative_qty(args):
limit 1 limit 1
""", args, as_dict=1) """, args, as_dict=1)
def get_future_sle_with_negative_batch_qty(args):
return frappe.db.sql("""
with batch_ledger as (
select
posting_date, posting_time, voucher_type, voucher_no,
sum(actual_qty) over (order by posting_date, posting_time, creation) as cumulative_total
from `tabStock Ledger Entry`
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
and batch_no=%(batch_no)s
and is_cancelled = 0
order by posting_date, posting_time, creation
)
select * from batch_ledger
where
cumulative_total < 0.0
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
limit 1
""", args, as_dict=1)
def _round_off_if_near_zero(number: float, precision: int = 6) -> float: def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
""" Rounds off the number to zero only if number is close to zero for decimal """ Rounds off the number to zero only if number is close to zero for decimal
specified in precision. Precision defaults to 6. specified in precision. Precision defaults to 6.

View File

@ -187,7 +187,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:
@ -203,11 +203,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")))