Merge pull request #42745 from frappe/version-15-hotfix

chore: release v15
This commit is contained in:
ruthra kumar
2024-08-14 13:31:20 +05:30
committed by GitHub
43 changed files with 746 additions and 146 deletions

View File

@@ -47,9 +47,11 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data])
if no_of_columns > 8:
if no_of_columns != 8:
frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"),
_(
"Columns are not according to template. Please compare the uploaded file with standard template"
),
title=(_("Wrong Template")),
)

View File

@@ -85,18 +85,16 @@ frappe.ui.form.on("Exchange Rate Revaluation", {
},
make_jv: function (frm) {
let revaluation_journal = null;
let zero_balance_journal = null;
frappe.call({
method: "make_jv_entries",
doc: frm.doc,
freeze: true,
freeze_message: "Making Journal Entries...",
freeze_message: __("Creating Journal Entries..."),
callback: function (r) {
if (r.message) {
let response = r.message;
if (response["revaluation_jv"] || response["zero_balance_jv"]) {
frappe.msgprint(__("Journals have been created"));
frappe.msgprint(__("Journal entries have been created"));
}
}
},

View File

@@ -230,7 +230,7 @@ frappe.ui.form.on("Payment Entry", {
hide_unhide_fields: function (frm) {
var company_currency = frm.doc.company
? frappe.get_doc(":Company", frm.doc.company).default_currency
? frappe.get_doc(":Company", frm.doc.company)?.default_currency
: "";
frm.toggle_display(

View File

@@ -338,6 +338,17 @@ class PaymentRequest(Document):
payment_entry.received_amount = amount
payment_entry.get("references")[0].allocated_amount = amount
# Update 'Paid Amount' on Forex transactions
if self.currency != ref_doc.company_currency:
if (
self.payment_request_type == "Outward"
and payment_entry.paid_from_account_currency == ref_doc.company_currency
and payment_entry.paid_from_account_currency != payment_entry.paid_to_account_currency
):
payment_entry.paid_amount = payment_entry.base_paid_amount = (
payment_entry.target_exchange_rate * payment_entry.received_amount
)
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})

View File

@@ -4,10 +4,12 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.utils import get_exchange_rate
@@ -32,7 +34,7 @@ payment_method = [
]
class TestPaymentRequest(unittest.TestCase):
class TestPaymentRequest(FrappeTestCase):
def setUp(self):
if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"):
frappe.get_doc(payment_gateway).insert(ignore_permissions=True)
@@ -260,3 +262,19 @@ class TestPaymentRequest(unittest.TestCase):
# Try to make Payment Request more than SO amount, should give validation
pr2.grand_total = 900
self.assertRaises(frappe.ValidationError, pr2.save)
def test_conversion_on_foreign_currency_accounts(self):
po_doc = create_purchase_order(supplier="_Test Supplier USD", currency="USD", do_not_submit=1)
po_doc.conversion_rate = 80
po_doc.items[0].qty = 1
po_doc.items[0].rate = 10
po_doc.save().submit()
pr = make_payment_request(dt=po_doc.doctype, dn=po_doc.name, recipient_id="nabin@erpnext.com")
pr = frappe.get_doc(pr).save().submit()
pe = pr.create_payment_entry()
self.assertEqual(pe.base_paid_amount, 800)
self.assertEqual(pe.paid_amount, 800)
self.assertEqual(pe.base_received_amount, 800)
self.assertEqual(pe.received_amount, 10)

View File

@@ -16,6 +16,7 @@
"cost_center",
"territory",
"ignore_exchange_rate_revaluation_journals",
"ignore_cr_dr_notes",
"column_break_14",
"to_date",
"finance_book",
@@ -383,10 +384,16 @@
"fieldname": "ignore_exchange_rate_revaluation_journals",
"fieldtype": "Check",
"label": "Ignore Exchange Rate Revaluation Journals"
},
{
"default": "0",
"fieldname": "ignore_cr_dr_notes",
"fieldtype": "Check",
"label": "Ignore System Generated Credit / Debit Notes"
}
],
"links": [],
"modified": "2023-12-18 12:20:08.965120",
"modified": "2024-08-13 10:41:18.381165",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",

View File

@@ -54,6 +54,7 @@ class ProcessStatementOfAccounts(Document):
frequency: DF.Literal["Weekly", "Monthly", "Quarterly"]
from_date: DF.Date | None
group_by: DF.Literal["", "Group by Voucher", "Group by Voucher (Consolidated)"]
ignore_cr_dr_notes: DF.Check
ignore_exchange_rate_revaluation_journals: DF.Check
include_ageing: DF.Check
include_break: DF.Check
@@ -133,6 +134,9 @@ def get_statement_dict(doc, get_statement_dict=False):
if doc.ignore_exchange_rate_revaluation_journals:
filters.update({"ignore_err": True})
if doc.ignore_cr_dr_notes:
filters.update({"ignore_cr_dr_notes": True})
if doc.report == "General Ledger":
filters.update(get_gl_filters(doc, entry, tax_id, presentation_currency))
col, res = get_soa(filters)

View File

@@ -57,6 +57,7 @@
"base_net_rate",
"base_net_amount",
"valuation_rate",
"sales_incoming_rate",
"item_tax_amount",
"landed_cost_voucher_amount",
"rm_supp_cost",
@@ -958,12 +959,22 @@
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
"fieldname": "sales_incoming_rate",
"fieldtype": "Currency",
"hidden": 1,
"label": "Sales Incoming Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-06-14 11:57:07.171700",
"modified": "2024-07-19 12:12:42.449298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -79,6 +79,7 @@ class PurchaseInvoiceItem(Document):
rejected_serial_no: DF.Text | None
rejected_warehouse: DF.Link | None
rm_supp_cost: DF.Currency
sales_incoming_rate: DF.Currency
sales_invoice_item: DF.Data | None
serial_and_batch_bundle: DF.Link | None
serial_no: DF.Text | None

View File

@@ -1316,6 +1316,10 @@ class SalesInvoice(SellingController):
for item in self.get("items"):
if flt(item.base_net_amount, item.precision("base_net_amount")):
# Do not book income for transfer within same company
if self.is_internal_transfer():
continue
if item.is_fixed_asset:
asset = self.get_asset(item)
@@ -1374,37 +1378,33 @@ class SalesInvoice(SellingController):
self.set_asset_status(asset)
else:
# Do not book income for transfer within same company
if not self.is_internal_transfer():
income_account = (
item.income_account
if (not item.enable_deferred_revenue or self.is_return)
else item.deferred_revenue_account
)
income_account = (
item.income_account
if (not item.enable_deferred_revenue or self.is_return)
else item.deferred_revenue_account
)
amount, base_amount = self.get_amount_and_base_amount(
item, enable_discount_accounting
)
amount, base_amount = self.get_amount_and_base_amount(item, enable_discount_accounting)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict(
{
"account": income_account,
"against": self.customer,
"credit": flt(base_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (
flt(base_amount, item.precision("base_net_amount"))
if account_currency == self.company_currency
else flt(amount, item.precision("net_amount"))
),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
account_currency,
item=item,
)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict(
{
"account": income_account,
"against": self.customer,
"credit": flt(base_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (
flt(base_amount, item.precision("base_net_amount"))
if account_currency == self.company_currency
else flt(amount, item.precision("net_amount"))
),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
account_currency,
item=item,
)
)
# expense account gl entries
if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company):
@@ -1479,6 +1479,10 @@ class SalesInvoice(SellingController):
if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
payment_mode.base_amount -= flt(self.change_amount)
against_voucher = self.name
if self.is_return and self.return_against and not self.update_outstanding_for_self:
against_voucher = self.return_against
if payment_mode.base_amount:
# POS, make payment entries
gl_entries.append(
@@ -1492,7 +1496,7 @@ class SalesInvoice(SellingController):
"credit_in_account_currency": payment_mode.base_amount
if self.party_account_currency == self.company_currency
else payment_mode.amount,
"against_voucher": self.name,
"against_voucher": against_voucher,
"against_voucher_type": self.doctype,
"cost_center": self.cost_center,
},

View File

@@ -5,6 +5,7 @@ import copy
import json
import frappe
from frappe import qb
from frappe.model.dynamic_links import get_dynamic_link_map
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, getdate, nowdate, today
@@ -3083,6 +3084,84 @@ class TestSalesInvoice(FrappeTestCase):
party_link.delete()
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
def test_sales_invoice_against_supplier_usd_with_dimensions(self):
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
make_customer,
)
from erpnext.accounts.doctype.party_link.party_link import create_party_link
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
# create a customer
customer = make_customer(customer="_Test Common Supplier USD")
cust_doc = frappe.get_doc("Customer", customer)
cust_doc.default_currency = "USD"
cust_doc.save()
# create a supplier
supplier = create_supplier(supplier_name="_Test Common Supplier USD").name
supp_doc = frappe.get_doc("Supplier", supplier)
supp_doc.default_currency = "USD"
supp_doc.save()
# create a party link between customer & supplier
party_link = create_party_link("Supplier", supplier, customer)
# enable common party accounting
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 1)
# create a dimension and make it mandatory
if not frappe.get_all("Accounting Dimension", filters={"document_type": "Department"}):
dim = frappe.get_doc(
{
"doctype": "Accounting Dimension",
"document_type": "Department",
"dimension_defaults": [{"company": "_Test Company", "mandatory_for_bs": True}],
}
)
dim.save()
else:
dim = frappe.get_doc(
"Accounting Dimension",
frappe.get_all("Accounting Dimension", filters={"document_type": "Department"})[0],
)
dim.disabled = False
dim.dimension_defaults = []
dim.append("dimension_defaults", {"company": "_Test Company", "mandatory_for_bs": True})
dim.save()
# create a sales invoice
si = create_sales_invoice(
customer=customer, parent_cost_center="_Test Cost Center - _TC", do_not_submit=True
)
si.department = "All Departments"
si.save().submit()
# check outstanding of sales invoice
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(flt(si.outstanding_amount), 0.0)
# check creation of journal entry
jv = frappe.get_all(
"Journal Entry Account",
{
"account": si.debit_to,
"party_type": "Customer",
"party": si.customer,
"reference_type": si.doctype,
"reference_name": si.name,
"department": "All Departments",
},
pluck="credit_in_account_currency",
)
self.assertTrue(jv)
self.assertEqual(jv[0], si.grand_total)
dim.disabled = True
dim.save()
party_link.delete()
frappe.db.set_single_value("Accounts Settings", "enable_common_party_accounting", 0)
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@@ -3758,6 +3837,40 @@ class TestSalesInvoice(FrappeTestCase):
]
self.assertEqual(expected, actual)
def test_pos_returns_without_update_outstanding_for_self(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
pos_profile = make_pos_profile()
pos_profile.payments = []
pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"})
pos_profile.save()
pos = create_sales_invoice(qty=10, do_not_save=True)
pos.is_pos = 1
pos.pos_profile = pos_profile.name
pos.append(
"payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500}
)
pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500})
pos.save().submit()
pos_return = make_sales_return(pos.name)
pos_return.update_outstanding_for_self = False
pos_return.save().submit()
gle = qb.DocType("GL Entry")
res = (
qb.from_(gle)
.select(gle.against_voucher)
.distinct()
.where(
gle.is_cancelled.eq(0) & gle.voucher_no.eq(pos_return.name) & gle.against_voucher.notnull()
)
.run(as_list=1)
)
self.assertEqual(len(res), 1)
self.assertEqual(res[0][0], pos_return.return_against)
def set_advance_flag(company, flag, default_account):
frappe.db.set_value(

View File

@@ -38,6 +38,12 @@ frappe.ui.form.on("Subscription", {
__("Actions")
);
frm.add_custom_button(
__("Force-Fetch Subscription Updates"),
() => frm.trigger("force_fetch_subscription_updates"),
__("Actions")
);
frm.add_custom_button(
__("Cancel Subscription"),
() => frm.trigger("cancel_this_subscription"),
@@ -82,4 +88,11 @@ frappe.ui.form.on("Subscription", {
}
});
},
force_fetch_subscription_updates: function (frm) {
frm.call("force_fetch_subscription_updates").then((r) => {
if (!r.exec) {
frm.reload_doc();
}
});
},
});

View File

@@ -717,6 +717,31 @@ class Subscription(Document):
self.update_subscription_period(posting_date or nowdate())
self.save()
@frappe.whitelist()
def force_fetch_subscription_updates(self):
"""
Process Subscription and create Invoices even if current date doesn't lie between current_invoice_start and currenct_invoice_end
It makes use of 'Proces Subscription' to force processing in a specific 'posting_date'
"""
# Don't process future subscriptions
if nowdate() < self.current_invoice_start:
frappe.msgprint(_("Subscription for Future dates cannot be processed."))
return
processing_date = None
if self.generate_invoice_at == "Beginning of the current subscription period":
processing_date = self.current_invoice_start
elif self.generate_invoice_at == "End of the current subscription period":
processing_date = self.current_invoice_end
elif self.generate_invoice_at == "Days before the current subscription period":
processing_date = add_days(self.current_invoice_start, -self.number_of_days)
process_subscription = frappe.new_doc("Process Subscription")
process_subscription.posting_date = processing_date
process_subscription.subscription = self.name
process_subscription.save().submit()
def is_prorate() -> int:
return cint(frappe.db.get_single_value("Subscription Settings", "prorate"))

View File

@@ -521,6 +521,18 @@ class TestSubscription(FrappeTestCase):
subscription.process(posting_date="2023-01-22")
self.assertEqual(len(subscription.invoices), 2)
def test_future_subscription(self):
"""Force-Fetch should not process future subscriptions"""
subscription = create_subscription(
start_date=add_months(nowdate(), 1),
submit_invoice=0,
generate_new_invoices_past_due_date=1,
party="_Test Subscription Customer John Doe",
)
subscription.force_fetch_subscription_updates()
subscription.reload()
self.assertEqual(len(subscription.invoices), 0)
def make_plans():
create_plan(plan_name="_Test Plan Name", cost=900, currency="INR")

View File

@@ -248,7 +248,10 @@ def get_conditions(filters):
as_list=True,
)
if system_generated_cr_dr_journals:
filters.update({"voucher_no_not_in": [x[0] for x in system_generated_cr_dr_journals]})
vouchers_to_ignore = (filters.get("voucher_no_not_in") or []) + [
x[0] for x in system_generated_cr_dr_journals
]
filters.update({"voucher_no_not_in": vouchers_to_ignore})
if filters.get("voucher_no_not_in"):
conditions.append("voucher_no not in %(voucher_no_not_in)s")

View File

@@ -234,7 +234,7 @@ class TestAsset(AssetSetup):
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0],
9000,
get_last_day(add_months(purchase_date, 1)),
add_days(get_last_day(add_months(purchase_date, 1)), 1),
date,
original_schedule_date=get_last_day(nowdate()),
)
@@ -320,7 +320,7 @@ class TestAsset(AssetSetup):
pro_rata_amount, _, _ = _get_pro_rata_amt(
asset.finance_books[0],
9000,
get_last_day(add_months(purchase_date, 1)),
add_days(get_last_day(add_months(purchase_date, 1)), 1),
date,
original_schedule_date=get_last_day(nowdate()),
)

View File

@@ -291,7 +291,9 @@ class AssetDepreciationSchedule(Document):
if skip_row:
continue
schedule_date = add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
schedule_date = get_last_day(
add_months(row.depreciation_start_date, n * cint(row.frequency_of_depreciation))
)
if not current_fiscal_year_end_date:
current_fiscal_year_end_date = get_fiscal_year(row.depreciation_start_date)[2]
elif getdate(schedule_date) > getdate(current_fiscal_year_end_date):
@@ -330,8 +332,10 @@ class AssetDepreciationSchedule(Document):
getdate(asset_doc.available_for_use_date),
(asset_doc.opening_number_of_booked_depreciations * row.frequency_of_depreciation),
)
if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
from_date = get_last_day(from_date)
if self.depreciation_schedule:
from_date = self.depreciation_schedule[-1].schedule_date
from_date = add_days(self.depreciation_schedule[-1].schedule_date, 1)
depreciation_amount, days, months = _get_pro_rata_amt(
row,
@@ -353,9 +357,8 @@ class AssetDepreciationSchedule(Document):
and not self.opening_accumulated_depreciation
and not self.flags.wdv_it_act_applied
):
from_date = add_days(
asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
from_date = asset_doc.available_for_use_date
# needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = _get_pro_rata_amt(
row,
depreciation_amount,
@@ -406,6 +409,8 @@ class AssetDepreciationSchedule(Document):
(n + self.opening_number_of_booked_depreciations)
* cint(row.frequency_of_depreciation),
)
if is_last_day_of_the_month(getdate(asset_doc.available_for_use_date)):
asset_doc.to_date = get_last_day(asset_doc.to_date)
depreciation_amount_without_pro_rata = depreciation_amount
@@ -421,7 +426,7 @@ class AssetDepreciationSchedule(Document):
depreciation_amount_without_pro_rata, depreciation_amount
)
schedule_date = add_days(schedule_date, days)
schedule_date = add_days(schedule_date, days - 1)
if not depreciation_amount:
continue
@@ -504,7 +509,10 @@ class AssetDepreciationSchedule(Document):
continue
if not accumulated_depreciation:
if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment:
if i > 0 and (
asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment
or asset_doc.flags.increase_in_asset_value_due_to_repair
):
accumulated_depreciation = self.get("depreciation_schedule")[
i - 1
].accumulated_depreciation_amount
@@ -553,9 +561,11 @@ def _check_is_pro_rata(asset_doc, row, wdv_or_dd_non_yearly=False):
# otherwise, if opening_number_of_booked_depreciations = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
if row.depreciation_method in ("Straight Line", "Manual"):
prev_depreciation_start_date = add_months(
row.depreciation_start_date,
(row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
prev_depreciation_start_date = get_last_day(
add_months(
row.depreciation_start_date,
(row.frequency_of_depreciation * -1) * asset_doc.opening_number_of_booked_depreciations,
)
)
from_date = asset_doc.available_for_use_date
days = date_diff(prev_depreciation_start_date, from_date) + 1
@@ -610,7 +620,7 @@ def _get_pro_rata_amt(
has_wdv_or_dd_non_yearly_pro_rata=False,
original_schedule_date=None,
):
days = date_diff(to_date, from_date)
days = date_diff(to_date, from_date) + 1
months = month_diff(to_date, from_date)
if has_wdv_or_dd_non_yearly_pro_rata:
total_days = get_total_days(original_schedule_date or to_date, 12)
@@ -670,7 +680,7 @@ def get_straight_line_or_manual_depr_amount(
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset value
elif asset.flags.increase_in_asset_value_due_to_repair:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_depreciations
number_of_pending_depreciations
)
# if the Depreciation Schedule is being modified after Asset Value Adjustment due to decrease in asset value
elif asset.flags.decrease_in_asset_value_due_to_value_adjustment:
@@ -1034,6 +1044,7 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
date_of_return=None,
value_after_depreciation=None,
ignore_booked_entry=False,
difference_amount=None,
):
for row in asset_doc.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
@@ -1048,6 +1059,8 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
)
new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
if asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment and not value_after_depreciation:
value_after_depreciation = row.value_after_depreciation + difference_amount
if asset_doc.flags.increase_in_asset_value_due_to_repair and row.depreciation_method in (
"Written Down Value",

View File

@@ -29,6 +29,15 @@ frappe.ui.form.on("Asset Repair", {
};
});
frm.set_query("purchase_invoice", function () {
return {
filters: {
company: frm.doc.company,
docstatus: 1,
},
};
});
frm.set_query("warehouse", "stock_items", function () {
return {
filters: {

View File

@@ -117,7 +117,9 @@ class AssetRepair(AccountsController):
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
make_new_active_asset_depr_schedules_and_cancel_current_ones(
self.asset_doc, notes, ignore_booked_entry=True
)
self.asset_doc.save()
add_asset_activity(
@@ -154,7 +156,9 @@ class AssetRepair(AccountsController):
get_link_to_form(self.doctype, self.name),
)
self.asset_doc.flags.ignore_validate_update_after_submit = True
make_new_active_asset_depr_schedules_and_cancel_current_ones(self.asset_doc, notes)
make_new_active_asset_depr_schedules_and_cancel_current_ones(
self.asset_doc, notes, ignore_booked_entry=True
)
self.asset_doc.save()
add_asset_activity(

View File

@@ -56,7 +56,8 @@ class AssetValueAdjustment(Document):
)
def on_cancel(self):
self.update_asset(self.current_asset_value)
frappe.get_doc("Journal Entry", self.journal_entry).cancel()
self.update_asset()
add_asset_activity(
self.asset,
_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
@@ -144,7 +145,7 @@ class AssetValueAdjustment(Document):
self.db_set("journal_entry", je.name)
def update_asset(self, asset_value):
def update_asset(self, asset_value=None):
asset = frappe.get_doc("Asset", self.asset)
if not asset.calculate_depreciation:
@@ -170,7 +171,11 @@ class AssetValueAdjustment(Document):
)
make_new_active_asset_depr_schedules_and_cancel_current_ones(
asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True
asset,
notes,
value_after_depreciation=asset_value,
ignore_booked_entry=True,
difference_amount=self.difference_amount,
)
asset.flags.ignore_validate_update_after_submit = True
asset.save()

View File

@@ -12,6 +12,7 @@ from erpnext.assets.doctype.asset.test_asset import create_asset_data
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_asset_depr_schedule_doc,
)
from erpnext.assets.doctype.asset_repair.test_asset_repair import create_asset_repair
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -128,6 +129,136 @@ class TestAssetValueAdjustment(unittest.TestCase):
self.assertEqual(schedules, expected_schedules)
def test_depreciation_after_cancelling_asset_repair(self):
pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=120000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, "name")
asset_doc = frappe.get_doc("Asset", asset_name)
asset_doc.calculate_depreciation = 1
asset_doc.available_for_use_date = "2023-01-15"
asset_doc.purchase_date = "2023-01-15"
asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line",
"total_number_of_depreciations": 12,
"frequency_of_depreciation": 1,
"depreciation_start_date": "2023-01-31",
},
)
asset_doc.submit()
post_depreciation_entries(getdate("2023-08-21"))
# create asset repair
asset_repair = create_asset_repair(asset=asset_doc, capitalize_repair_cost=1, submit=1)
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Active")
# create asset value adjustment
current_value = get_asset_value_after_depreciation(asset_doc.name)
adj_doc = make_asset_value_adjustment(
asset=asset_doc.name,
current_asset_value=current_value,
new_asset_value=50000.0,
date="2023-08-21",
)
adj_doc.submit()
first_asset_depr_schedule.load_from_db()
second_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEqual(second_asset_depr_schedule.status, "Active")
self.assertEqual(first_asset_depr_schedule.status, "Cancelled")
# Test gl entry creted from asset value adjustemnet
expected_gle = (
("_Test Accumulated Depreciations - _TC", 0.0, 5625.29),
("_Test Depreciations - _TC", 5625.29, 0.0),
)
gle = frappe.db.sql(
"""select account, debit, credit from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
order by account""",
adj_doc.journal_entry,
)
self.assertSequenceEqual(gle, expected_gle)
# test depreciation schedule after asset repair and asset value adjustemnet
expected_schedules = [
["2023-01-31", 5474.73, 5474.73],
["2023-02-28", 9983.33, 15458.06],
["2023-03-31", 9983.33, 25441.39],
["2023-04-30", 9983.33, 35424.72],
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 2766.67, 68141.38],
["2023-09-30", 2766.67, 70908.05],
["2023-10-31", 2766.67, 73674.72],
["2023-11-30", 2766.67, 76441.39],
["2023-12-31", 2766.67, 79208.06],
["2024-01-31", 2766.67, 81974.73],
["2024-02-29", 2766.67, 84741.4],
["2024-03-31", 2766.67, 87508.07],
["2024-04-30", 2766.67, 90274.74],
["2024-05-31", 2766.67, 93041.41],
["2024-06-30", 2766.67, 95808.08],
["2024-07-31", 2766.67, 98574.75],
["2024-08-31", 2766.67, 101341.42],
["2024-09-30", 2766.67, 104108.09],
["2024-10-31", 2766.67, 106874.76],
["2024-11-30", 2766.67, 109641.43],
["2024-12-31", 2766.67, 112408.1],
["2025-01-15", 2766.61, 115174.71],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in second_asset_depr_schedule.get("depreciation_schedule")
]
self.assertEqual(schedules, expected_schedules)
# Cancel asset repair
asset_repair.cancel()
asset_repair.load_from_db()
second_asset_depr_schedule.load_from_db()
third_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEqual(third_asset_depr_schedule.status, "Active")
self.assertEqual(second_asset_depr_schedule.status, "Cancelled")
# After cancelling asset repair asset life will be decreased and new depreciation schedule should be calculated
expected_schedules = [
["2023-01-31", 5474.73, 5474.73],
["2023-02-28", 9983.33, 15458.06],
["2023-03-31", 9983.33, 25441.39],
["2023-04-30", 9983.33, 35424.72],
["2023-05-31", 9983.33, 45408.05],
["2023-06-30", 9983.33, 55391.38],
["2023-07-31", 9983.33, 65374.71],
["2023-08-31", 8133.33, 73508.04],
["2023-09-30", 8133.33, 81641.37],
["2023-10-31", 8133.33, 89774.7],
["2023-11-30", 8133.33, 97908.03],
["2023-12-31", 8133.33, 106041.36],
["2024-01-15", 8133.35, 114174.71],
]
schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]
for d in third_asset_depr_schedule.get("depreciation_schedule")
]
self.assertEqual(schedules, expected_schedules)
def make_asset_value_adjustment(**args):
args = frappe._dict(args)

View File

@@ -2433,6 +2433,15 @@ class AccountsController(TransactionBase):
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
advance_entry.is_advance = "Yes"
# update dimesions
dimensions_dict = frappe._dict()
active_dimensions = get_dimensions()[0]
for dim in active_dimensions:
dimensions_dict[dim.fieldname] = self.get(dim.fieldname)
reconcilation_entry.update(dimensions_dict)
advance_entry.update(dimensions_dict)
if self.doctype == "Sales Invoice":
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
advance_entry.debit_in_account_currency = self.outstanding_amount

View File

@@ -314,18 +314,22 @@ class BuyingController(SubcontractingController):
get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
)
net_rate = item.base_net_amount
if item.sales_incoming_rate: # for internal transfer
net_rate = item.qty * item.sales_incoming_rate
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
if self.get("is_old_subcontracting_flow"):
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = (
item.base_net_amount
net_rate
+ item.item_tax_amount
+ item.rm_supp_cost
+ flt(item.landed_cost_voucher_amount)
) / qty_in_stock_uom
else:
item.valuation_rate = (
item.base_net_amount
net_rate
+ item.item_tax_amount
+ flt(item.landed_cost_voucher_amount)
+ flt(item.get("rate_difference_with_purchase_invoice"))
@@ -336,72 +340,88 @@ class BuyingController(SubcontractingController):
update_regional_item_valuation_rate(self)
def set_incoming_rate(self):
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
"""
Override item rate with incoming rate for internal stock transfer
"""
if self.doctype not in ("Purchase Receipt", "Purchase Invoice"):
return
if not (self.doctype == "Purchase Receipt" or self.get("update_stock")):
return
if cint(self.get("is_return")):
# Get outgoing rate based on original item cost based on valuation method
return
if not self.is_internal_transfer():
return
allow_at_arms_length_price = frappe.get_cached_value(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
)
if allow_at_arms_length_price:
return
self.set_sales_incoming_rate_for_internal_transfer()
for d in self.get("items"):
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
if d.rate == d.sales_incoming_rate:
continue
d.rate = d.sales_incoming_rate
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
def set_sales_incoming_rate_for_internal_transfer(self):
"""
Set incoming rate from the sales transaction against which the
purchase is made (internal transfer)
"""
ref_doctype_map = {
"Purchase Order": "Sales Order Item",
"Purchase Receipt": "Delivery Note Item",
"Purchase Invoice": "Sales Invoice Item",
}
ref_doctype = ref_doctype_map.get(self.doctype)
items = self.get("items")
for d in items:
if not cint(self.get("is_return")):
# Get outgoing rate based on original item cost based on valuation method
for d in self.get("items"):
if not d.get(frappe.scrub(ref_doctype)):
posting_time = self.get("posting_time")
if not posting_time:
posting_time = nowtime()
if not d.get(frappe.scrub(ref_doctype)):
posting_time = self.get("posting_time")
if not posting_time and self.doctype == "Purchase Order":
posting_time = nowtime()
outgoing_rate = get_incoming_rate(
{
"item_code": d.item_code,
"warehouse": d.get("from_warehouse"),
"posting_date": self.get("posting_date") or self.get("transaction_date"),
"posting_time": posting_time,
"qty": -1 * flt(d.get("stock_qty")),
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
"allow_zero_valuation": d.get("allow_zero_valuation"),
"voucher_detail_no": d.name,
},
raise_error_if_no_rate=False,
)
outgoing_rate = get_incoming_rate(
{
"item_code": d.item_code,
"warehouse": d.get("from_warehouse"),
"posting_date": self.get("posting_date") or self.get("transaction_date"),
"posting_time": posting_time,
"qty": -1 * flt(d.get("stock_qty")),
"serial_and_batch_bundle": d.get("serial_and_batch_bundle"),
"company": self.company,
"voucher_type": self.doctype,
"voucher_no": self.name,
"allow_zero_valuation": d.get("allow_zero_valuation"),
"voucher_detail_no": d.name,
},
raise_error_if_no_rate=False,
)
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else:
field = (
"incoming_rate"
if self.get("is_internal_supplier") and not self.doctype == "Purchase Order"
else "rate"
)
rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
* (d.conversion_factor or 1),
d.precision("rate"),
)
if self.is_internal_transfer():
if self.doctype == "Purchase Receipt" or self.get("update_stock"):
if rate != d.rate:
d.rate = rate
frappe.msgprint(
_(
"Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer"
).format(d.idx),
alert=1,
)
d.discount_percentage = 0.0
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
d.sales_incoming_rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
else:
field = "incoming_rate" if self.get("is_internal_supplier") else "rate"
d.sales_incoming_rate = flt(
frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), field)
* (d.conversion_factor or 1),
d.precision("rate"),
)
def validate_for_subcontracting(self):
if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
@@ -566,11 +586,9 @@ class BuyingController(SubcontractingController):
if d.from_warehouse:
sle.dependant_sle_voucher_detail_no = d.name
else:
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
sle.update(
{
"incoming_rate": incoming_rate,
"incoming_rate": d.valuation_rate,
"recalculate_rate": 1
if (self.is_subcontracted and (d.bom or d.get("fg_item"))) or d.from_warehouse
else 0,

View File

@@ -435,6 +435,9 @@ class SellingController(StockController):
if self.doctype not in ("Delivery Note", "Sales Invoice"):
return
allow_at_arms_length_price = frappe.get_cached_value(
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
)
items = self.get("items") + (self.get("packed_items") or [])
for d in items:
if not frappe.get_cached_value("Item", d.item_code, "is_stock_item"):
@@ -481,6 +484,9 @@ class SellingController(StockController):
if d.incoming_rate != incoming_rate:
d.incoming_rate = incoming_rate
else:
if allow_at_arms_length_price:
continue
rate = flt(
flt(d.incoming_rate, d.precision("incoming_rate")) * d.conversion_factor,
d.precision("rate"),

View File

@@ -5,7 +5,7 @@
import frappe
from frappe import qb
from frappe.query_builder.functions import Sum
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, getdate, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -13,6 +13,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import prepare_data_for_internal_transfer
from erpnext.stock.doctype.item.test_item import create_item
@@ -804,6 +805,41 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
@change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1})
def test_16_internal_transfer_at_arms_length_price(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
prepare_data_for_internal_transfer()
company = "_Test Company with perpetual inventory"
target_warehouse = create_warehouse("_Test Internal Warehouse New 1", company=company)
warehouse = create_warehouse("_Test Internal Warehouse New 2", company=company)
arms_length_price = 40
si = create_sales_invoice(
company=company,
customer="_Test Internal Customer 2",
debit_to="Debtors - TCP1",
target_warehouse=target_warehouse,
warehouse=warehouse,
income_account="Sales - TCP1",
expense_account="Cost of Goods Sold - TCP1",
cost_center="Main - TCP1",
update_stock=True,
do_not_save=True,
do_not_submit=True,
)
si.items[0].rate = arms_length_price
si.save()
# rate should not reset to incoming rate
self.assertEqual(si.items[0].rate, arms_length_price)
frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 0)
si.items[0].rate = arms_length_price
si.save()
# rate should reset to incoming rate
self.assertEqual(si.items[0].rate, 100)
def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)

View File

@@ -93,7 +93,26 @@ class Opportunity(TransactionBase, CRMNote):
def onload(self):
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
load_address_and_contact(ref_doc)
load_address_and_contact(self)
ref_doc_contact_list = ref_doc.get("__onload").get("contact_list")
opportunity_doc_contact_list = [
contact
for contact in self.get("__onload").get("contact_list")
if contact not in ref_doc_contact_list
]
ref_doc_contact_list.extend(opportunity_doc_contact_list)
ref_doc.set_onload("contact_list", ref_doc_contact_list)
ref_doc_addr_list = ref_doc.get("__onload").get("addr_list")
opportunity_doc_addr_list = [
addr for addr in self.get("__onload").get("addr_list") if addr not in ref_doc_addr_list
]
ref_doc_addr_list.extend(opportunity_doc_addr_list)
ref_doc.set_onload("addr_list", ref_doc_addr_list)
self.set("__onload", ref_doc.get("__onload"))
def after_insert(self):

View File

@@ -108,7 +108,9 @@ class OpportunitySummaryBySalesStage:
self.grouped_data = []
grouping_key = lambda o: (o["sales_stage"], o[based_on]) # noqa
for (sales_stage, _based_on), rows in groupby(self.query_result, grouping_key):
for (sales_stage, _based_on), rows in groupby(
sorted(self.query_result, key=grouping_key), key=grouping_key
):
self.grouped_data.append(
{
"sales_stage": sales_stage,

View File

@@ -122,7 +122,9 @@ class SalesPipelineAnalytics:
self.grouped_data = []
grouping_key = lambda o: (o.get(self.pipeline_by) or "Not Assigned", o[self.period_by]) # noqa
for (pipeline_by, period_by), rows in groupby(self.query_result, grouping_key):
for (pipeline_by, period_by), rows in groupby(
sorted(self.query_result, key=grouping_key), grouping_key
):
self.grouped_data.append(
{
self.pipeline_by: pipeline_by,

View File

@@ -357,6 +357,7 @@ erpnext.patches.v14_0.create_accounting_dimensions_in_reconciliation_tool
erpnext.patches.v15_0.allow_on_submit_dimensions_for_repostable_doctypes
erpnext.patches.v14_0.update_flag_for_return_invoices #2024-03-22
erpnext.patches.v15_0.create_accounting_dimensions_in_payment_request
erpnext.patches.v14_0.update_pos_return_ledger_entries
# below migration patch should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
erpnext.stock.doctype.delivery_note.patches.drop_unused_return_against_index # 2023-12-20

View File

@@ -0,0 +1,61 @@
import frappe
from frappe import qb
def execute():
sinv = qb.DocType("Sales Invoice")
pos_returns_without_self = (
qb.from_(sinv)
.select(sinv.name)
.where(
sinv.docstatus.eq(1)
& sinv.is_pos.eq(1)
& sinv.is_return.eq(1)
& sinv.return_against.notnull()
& sinv.update_outstanding_for_self.eq(0)
)
.run()
)
if pos_returns_without_self:
pos_returns_without_self = [x[0] for x in pos_returns_without_self]
gle = qb.DocType("GL Entry")
gl_against_references = (
qb.from_(gle)
.select(gle.voucher_no, gle.against_voucher)
.where(
gle.voucher_no.isin(pos_returns_without_self)
& gle.against_voucher.notnull()
& gle.against_voucher.eq(gle.voucher_no)
& gle.is_cancelled.eq(0)
)
.run()
)
_vouchers = list(set([x[0] for x in gl_against_references]))
invoice_return_against = (
qb.from_(sinv)
.select(sinv.name, sinv.return_against)
.where(sinv.name.isin(_vouchers) & sinv.return_against.notnull())
.orderby(sinv.name)
.run()
)
valid_references = set(invoice_return_against)
actual_references = set(gl_against_references)
invalid_references = actual_references.difference(valid_references)
if invalid_references:
# Repost Accounting Ledger
pos_for_reposting = (
qb.from_(sinv)
.select(sinv.company, sinv.name)
.where(sinv.name.isin([x[0] for x in invalid_references]))
.run(as_dict=True)
)
for x in pos_for_reposting:
ral = frappe.new_doc("Repost Accounting Ledger")
ral.company = x.company
ral.append("vouchers", {"voucher_type": "Sales Invoice", "voucher_no": x.name})
ral.save().submit()

View File

@@ -379,6 +379,9 @@ def make_sales_invoice(source_name, item_code=None, customer=None, currency=None
target.project = timesheet.parent_project
if customer:
target.customer = customer
default_price_list = frappe.get_value("Customer", customer, "default_price_list")
if default_price_list:
target.selling_price_list = default_price_list
if currency:
target.currency = currency

View File

@@ -840,6 +840,10 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
var get_party_currency = function() {
if (me.is_a_mapped_document()) {
return;
}
var party_type = frappe.meta.has_field(me.frm.doc.doctype, "customer") ? "Customer" : "Supplier";
var party_name = me.frm.doc[party_type.toLowerCase()];
if (party_name) {
@@ -1249,16 +1253,24 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
is_a_mapped_document(item) {
const mapped_item_field_map = {
"Delivery Note Item": ["si_detail", "so_detail", "dn_detail"],
"Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"],
"Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
"Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"],
"Delivery Note": ["si_detail", "so_detail", "dn_detail"],
"Sales Invoice": ["dn_detail", "so_detail", "sales_invoice_item"],
"Purchase Receipt": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"],
"Purchase Invoice": ["purchase_order_item", "pr_detail", "po_detail"],
"Sales Order": ["prevdoc_docname", "quotation_item"],
};
const mappped_fields = mapped_item_field_map[item.doctype] || [];
const mappped_fields = mapped_item_field_map[this.frm.doc.doctype] || [];
return mappped_fields
.map((field) => item[field])
.filter(Boolean).length > 0;
if (item) {
return mappped_fields
.map((field) => item[field])
.filter(Boolean).length > 0;
} else if (this.frm.doc?.items) {
let first_row = this.frm.doc.items[0];
let mapped_rows = mappped_fields.filter(d => first_row[d])
return mapped_rows?.length > 0;
}
}
batch_no(doc, cdt, cdn) {

View File

@@ -248,7 +248,7 @@ erpnext.SalesFunnel = class SalesFunnel {
context.fill();
// draw text
context.fillStyle = "black";
context.fillStyle = getComputedStyle(document.body).getPropertyValue("--text-color");
context.textBaseline = "middle";
context.font = "1.1em sans-serif";
context.fillText(__(title), width + 20, y_mid);

View File

@@ -93,7 +93,7 @@ def get_opp_by_lead_source(from_date, to_date, company):
summary = {}
sales_stages = set()
group_key = lambda o: (o["source"], o["sales_stage"]) # noqa
for (source, sales_stage), rows in groupby(cp_opportunities, group_key):
for (source, sales_stage), rows in groupby(sorted(cp_opportunities, key=group_key), group_key):
summary.setdefault(source, {})[sales_stage] = sum(r["compound_amount"] for r in rows)
sales_stages.add(sales_stage)

View File

@@ -328,6 +328,9 @@ class DeliveryNote(SellingController):
return
for item in self.items:
if item.use_serial_batch_fields:
continue
if item.pick_list_item and not item.serial_and_batch_bundle:
filters = {
"item_code": item.item_code,

View File

@@ -1097,7 +1097,8 @@ def create_delivery_note(source_name, target_doc=None):
)
)
for customer, rows in groupby(sales_orders, key=lambda so: so["customer"]):
group_key = lambda so: so["customer"] # noqa
for customer, rows in groupby(sorted(sales_orders, key=group_key), key=group_key):
sales_dict[customer] = {row.sales_order for row in rows}
if sales_dict:
@@ -1178,6 +1179,7 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
dn_item.batch_no = location.batch_no
dn_item.serial_no = location.serial_no
dn_item.use_serial_batch_fields = location.use_serial_batch_fields
update_delivery_note_item(source_doc, dn_item, delivery_note)

View File

@@ -67,6 +67,7 @@
"base_net_rate",
"base_net_amount",
"valuation_rate",
"sales_incoming_rate",
"item_tax_amount",
"rm_supp_cost",
"landed_cost_voucher_amount",
@@ -1124,12 +1125,22 @@
"fieldtype": "Check",
"label": "Return Qty from Rejected Warehouse",
"read_only": 1
},
{
"description": "Valuation rate for the item as per Sales Invoice (Only for Internal Transfers)",
"fieldname": "sales_incoming_rate",
"fieldtype": "Currency",
"hidden": 1,
"label": "Sales Incoming Rate",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2024-05-28 09:48:24.448815",
"modified": "2024-07-19 12:14:21.521466",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",

View File

@@ -88,6 +88,7 @@ class PurchaseReceiptItem(Document):
return_qty_from_rejected_warehouse: DF.Check
returned_qty: DF.Float
rm_supp_cost: DF.Currency
sales_incoming_rate: DF.Currency
sales_order: DF.Link | None
sales_order_item: DF.Data | None
sample_quantity: DF.Int

View File

@@ -303,7 +303,7 @@
"depends_on": "from_warehouse",
"fieldname": "source_warehouse_address",
"fieldtype": "Link",
"label": "Source Warehouse Address",
"label": "Source Warehouse Address Link",
"options": "Address"
},
{
@@ -333,7 +333,7 @@
"depends_on": "to_warehouse",
"fieldname": "target_warehouse_address",
"fieldtype": "Link",
"label": "Target Warehouse Address",
"label": "Target Warehouse Address Link",
"options": "Address"
},
{
@@ -686,10 +686,10 @@
"read_only": 1
},
{
"fieldname": "tab_connections",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
"fieldname": "tab_connections",
"fieldtype": "Tab Break",
"label": "Connections",
"show_dashboard": 1
}
],
"icon": "fa fa-file-text",
@@ -697,7 +697,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-06-26 19:12:17.937088",
"modified": "2024-08-13 19:02:42.386955",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",

View File

@@ -32,6 +32,7 @@
"allow_negative_stock",
"show_barcode_field",
"clean_description_html",
"allow_internal_transfer_at_arms_length_price",
"quality_inspection_settings_section",
"action_if_quality_inspection_is_not_submitted",
"column_break_23",
@@ -439,6 +440,13 @@
"fieldtype": "Check",
"label": "Do Not Update Serial / Batch on Creation of Auto Bundle"
},
{
"default": "0",
"description": "If enabled, the item rate won't adjust to the valuation rate during internal transfers, but accounting will still use the valuation rate.",
"fieldname": "allow_internal_transfer_at_arms_length_price",
"fieldtype": "Check",
"label": "Allow Internal Transfers at Arm's Length Price"
},
{
"default": "0",
"depends_on": "eval:doc.valuation_method === \"Moving Average\"",

View File

@@ -27,6 +27,7 @@ class StockSettings(Document):
action_if_quality_inspection_is_rejected: DF.Literal["Stop", "Warn"]
allow_from_dn: DF.Check
allow_from_pr: DF.Check
allow_internal_transfer_at_arms_length_price: DF.Check
allow_negative_stock: DF.Check
allow_partial_reservation: DF.Check
allow_to_edit_stock_uom_qty_for_purchase: DF.Check

View File

@@ -6,13 +6,14 @@ import gzip
import json
import frappe
from frappe import _, scrub
from frappe import _, bold, scrub
from frappe.model.meta import get_field_precision
from frappe.query_builder.functions import Sum
from frappe.utils import (
cint,
cstr,
flt,
format_date,
get_link_to_form,
getdate,
now,
@@ -743,9 +744,29 @@ class update_entries_after:
return self.distinct_item_warehouses[key].dependent_voucher_detail_nos
def validate_previous_sle_qty(self, sle):
previous_sle = self.data[sle.warehouse].previous_sle
if previous_sle and previous_sle.get("qty_after_transaction") < 0 and sle.get("actual_qty") > 0:
frappe.msgprint(
_(
"The stock for the item {0} in the {1} warehouse was negative on the {2}. You should create a positive entry {3} before the date {4} and time {5} to post the correct valuation rate. For more details, please read the <a href='https://docs.erpnext.com/docs/user/manual/en/stock-adjustment-cogs-with-negative-stock'>documentation<a>."
).format(
bold(sle.item_code),
bold(sle.warehouse),
bold(format_date(previous_sle.posting_date)),
sle.voucher_no,
bold(format_date(previous_sle.posting_date)),
bold(previous_sle.posting_time),
),
title=_("Warning on Negative Stock"),
indicator="blue",
)
def process_sle(self, sle):
# previous sle data for this warehouse
self.wh_data = self.data[sle.warehouse]
self.validate_previous_sle_qty(sle)
self.affected_transactions.add((sle.voucher_type, sle.voucher_no))
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):

View File

@@ -1334,7 +1334,7 @@ Lead to Quotation,Vom Lead zum Angebot,
Learn,Lernen,
Leave Management,Urlaube verwalten,
Leave and Attendance,Urlaub und Anwesenheit,
Leave application {0} already exists against the student {1},Verlassen der Anwendung {0} ist bereits für den Schüler {1} vorhanden,
Leave application {0} already exists against the student {1},Abwesenheitsantrag {0} existiert bereits für den Schüler {1},
Leaves has been granted sucessfully,Urlaub wurde genehmigt,
Leaves must be allocated in multiples of 0.5,"Abwesenheiten müssen ein Vielfaches von 0,5 sein",
Ledger,Hauptbuch,
@@ -5529,10 +5529,10 @@ Guardian Details,Erziehungsberechtigten-Details,
Guardians,Erziehungsberechtigte,
Sibling Details,Geschwister-Details,
Siblings,Geschwister,
Exit,Verlassen,
Exit,Austritt,
Date of Leaving,Austrittsdatum,
Leaving Certificate Number,Leaving Certificate Nummer,
Reason For Leaving,Grund für das Verlassen,
Reason For Leaving,Grund für den Austritt,
Student Admission,Studenten Eintritt,
Admission Start Date,Stichtag zum Zulassungsbeginn,
Admission End Date,Stichtag für Zulassungsende,
@@ -8795,13 +8795,13 @@ Warehouse wise Stock Value,Warenwert nach Lager,
Ex Works,Ab Werk,
Free Carrier,Frei Frachtführer,
Free Alongside Ship,Frei Längsseite Schiff,
Free on Board,Frei an Bord,
Free On Board,Frei an Bord,
Carriage Paid To,Frachtfrei,
Carriage and Insurance Paid to,Frachtfrei versichert,
Cost and Freight,Kosten und Fracht,
"Cost, Insurance and Freight","Kosten, Versicherung und Fracht",
Delivered at Place,Geliefert benannter Ort,
Delivered at Place Unloaded,Geliefert benannter Ort entladen,
Delivered At Place,Geliefert benannter Ort,
Delivered At Place Unloaded,Geliefert benannter Ort entladen,
Delivered Duty Paid,Geliefert verzollt,
Discount Validity,Frist für den Rabatt,
Discount Validity Based On,Frist für den Rabatt berechnet sich nach,
Can't render this file because it is too large.