mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-18 00:55:02 +00:00
Merge pull request #35571 from frappe/version-14-hotfix
chore: release v14
This commit is contained in:
@@ -12,6 +12,7 @@ from frappe.utils import flt, get_link_to_form
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
|
||||
from erpnext.accounts.utils import get_currency_precision
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
|
||||
@@ -170,6 +171,15 @@ class ExchangeRateRevaluation(Document):
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
# round off balance based on currency precision
|
||||
currency_precision = get_currency_precision()
|
||||
for acc in account_details:
|
||||
acc.balance_in_account_currency = flt(acc.balance_in_account_currency, currency_precision)
|
||||
acc.balance = flt(acc.balance, currency_precision)
|
||||
acc.zero_balance = (
|
||||
True if (acc.balance == 0 or acc.balance_in_account_currency == 0) else False
|
||||
)
|
||||
|
||||
return account_details
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -169,21 +169,18 @@ class PeriodClosingVoucher(AccountsController):
|
||||
return frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
t2.account_currency,
|
||||
t1.account_currency,
|
||||
{dimension_fields},
|
||||
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
|
||||
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
|
||||
from `tabGL Entry` t1, `tabAccount` t2
|
||||
from `tabGL Entry` t1
|
||||
where
|
||||
t1.is_cancelled = 0
|
||||
and t1.account = t2.name
|
||||
and t2.report_type = 'Profit and Loss'
|
||||
and t2.docstatus < 2
|
||||
and t2.company = %s
|
||||
and t1.account in (select name from `tabAccount` where report_type = 'Profit and Loss' and docstatus < 2 and company = %s)
|
||||
and t1.posting_date between %s and %s
|
||||
group by {dimension_fields}
|
||||
""".format(
|
||||
dimension_fields=", ".join(dimension_fields)
|
||||
dimension_fields=", ".join(dimension_fields),
|
||||
),
|
||||
(self.company, self.get("year_start_date"), self.posting_date),
|
||||
as_dict=1,
|
||||
|
||||
@@ -469,7 +469,7 @@
|
||||
"options": "UOM"
|
||||
},
|
||||
{
|
||||
"description": "If rate is zero them item will be treated as \"Free Item\"",
|
||||
"description": "If rate is zero then item will be treated as \"Free Item\"",
|
||||
"fieldname": "free_item_rate",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Free Item Rate"
|
||||
@@ -670,4 +670,4 @@
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "title"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -647,12 +647,12 @@ def set_taxes(
|
||||
else:
|
||||
args.update(get_party_details(party, party_type))
|
||||
|
||||
if party_type in ("Customer", "Lead"):
|
||||
if party_type in ("Customer", "Lead", "Prospect"):
|
||||
args.update({"tax_type": "Sales"})
|
||||
|
||||
if party_type == "Lead":
|
||||
if party_type in ["Lead", "Prospect"]:
|
||||
args["customer"] = None
|
||||
del args["lead"]
|
||||
del args[frappe.scrub(party_type)]
|
||||
else:
|
||||
args.update({"tax_type": "Purchase"})
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb, scrub
|
||||
@@ -849,30 +850,30 @@ class GrossProfitGenerator(object):
|
||||
Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children.
|
||||
"""
|
||||
|
||||
parents = []
|
||||
grouped = OrderedDict()
|
||||
|
||||
for row in self.si_list:
|
||||
if row.parent not in parents:
|
||||
parents.append(row.parent)
|
||||
# initialize list with a header row for each new parent
|
||||
grouped.setdefault(row.parent, [self.get_invoice_row(row)]).append(
|
||||
row.update(
|
||||
{"indent": 1.0, "parent_invoice": row.parent, "invoice_or_item": row.item_code}
|
||||
) # descendant rows will have indent: 1.0 or greater
|
||||
)
|
||||
|
||||
parents_index = 0
|
||||
for index, row in enumerate(self.si_list):
|
||||
if parents_index < len(parents) and row.parent == parents[parents_index]:
|
||||
invoice = self.get_invoice_row(row)
|
||||
self.si_list.insert(index, invoice)
|
||||
parents_index += 1
|
||||
# if item is a bundle, add it's components as seperate rows
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
bundled_items = self.get_bundle_items(row)
|
||||
for x in bundled_items:
|
||||
bundle_item = self.get_bundle_item_row(row, x)
|
||||
grouped.get(row.parent).append(bundle_item)
|
||||
|
||||
else:
|
||||
# skipping the bundle items rows
|
||||
if not row.indent:
|
||||
row.indent = 1.0
|
||||
row.parent_invoice = row.parent
|
||||
row.invoice_or_item = row.item_code
|
||||
self.si_list.clear()
|
||||
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
self.add_bundle_items(row, index)
|
||||
for items in grouped.values():
|
||||
self.si_list.extend(items)
|
||||
|
||||
def get_invoice_row(self, row):
|
||||
# header row format
|
||||
return frappe._dict(
|
||||
{
|
||||
"parent_invoice": "",
|
||||
@@ -901,13 +902,6 @@ class GrossProfitGenerator(object):
|
||||
}
|
||||
)
|
||||
|
||||
def add_bundle_items(self, product_bundle, index):
|
||||
bundle_items = self.get_bundle_items(product_bundle)
|
||||
|
||||
for i, item in enumerate(bundle_items):
|
||||
bundle_item = self.get_bundle_item_row(product_bundle, item)
|
||||
self.si_list.insert((index + i + 1), bundle_item)
|
||||
|
||||
def get_bundle_items(self, product_bundle):
|
||||
return frappe.get_all(
|
||||
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]
|
||||
|
||||
@@ -96,7 +96,6 @@ class AssetCategory(Document):
|
||||
frappe.throw(msg, title=_("Missing Account"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_asset_category_account(
|
||||
fieldname, item=None, asset=None, account=None, asset_category=None, company=None
|
||||
):
|
||||
|
||||
@@ -758,6 +758,7 @@ class AccountsController(TransactionBase):
|
||||
}
|
||||
)
|
||||
|
||||
update_gl_dict_with_regional_fields(self, gl_dict)
|
||||
accounting_dimensions = get_accounting_dimensions()
|
||||
dimension_dict = frappe._dict()
|
||||
|
||||
@@ -2835,3 +2836,8 @@ def validate_regional(doc):
|
||||
@erpnext.allow_regional
|
||||
def validate_einvoice_fields(doc):
|
||||
pass
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def update_gl_dict_with_regional_fields(doc, gl_dict):
|
||||
pass
|
||||
|
||||
@@ -43,7 +43,6 @@ class SellingController(StockController):
|
||||
self.validate_auto_repeat_subscription_dates()
|
||||
|
||||
def set_missing_values(self, for_validate=False):
|
||||
|
||||
super(SellingController, self).set_missing_values(for_validate)
|
||||
|
||||
# set contact and address details for customer, if they are not mentioned
|
||||
@@ -62,7 +61,7 @@ class SellingController(StockController):
|
||||
elif self.doctype == "Quotation" and self.party_name:
|
||||
if self.quotation_to == "Customer":
|
||||
customer = self.party_name
|
||||
else:
|
||||
elif self.quotation_to == "Lead":
|
||||
lead = self.party_name
|
||||
|
||||
if customer:
|
||||
|
||||
@@ -160,4 +160,3 @@ class TestLoanDisbursement(unittest.TestCase):
|
||||
interest = per_day_interest * 15
|
||||
|
||||
self.assertEqual(amounts["pending_principal_amount"], 1500000)
|
||||
self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2))
|
||||
|
||||
@@ -22,7 +22,7 @@ class LoanInterestAccrual(AccountsController):
|
||||
frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
|
||||
|
||||
if not self.last_accrual_date:
|
||||
self.last_accrual_date = get_last_accrual_date(self.loan)
|
||||
self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date)
|
||||
|
||||
def on_submit(self):
|
||||
self.make_gl_entries()
|
||||
@@ -271,14 +271,14 @@ def make_loan_interest_accrual_entry(args):
|
||||
|
||||
|
||||
def get_no_of_days_for_interest_accural(loan, posting_date):
|
||||
last_interest_accrual_date = get_last_accrual_date(loan.name)
|
||||
last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date)
|
||||
|
||||
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
|
||||
|
||||
return no_of_days
|
||||
|
||||
|
||||
def get_last_accrual_date(loan):
|
||||
def get_last_accrual_date(loan, posting_date):
|
||||
last_posting_date = frappe.db.sql(
|
||||
""" SELECT MAX(posting_date) from `tabLoan Interest Accrual`
|
||||
WHERE loan = %s and docstatus = 1""",
|
||||
@@ -286,12 +286,30 @@ def get_last_accrual_date(loan):
|
||||
)
|
||||
|
||||
if last_posting_date[0][0]:
|
||||
last_interest_accrual_date = last_posting_date[0][0]
|
||||
# interest for last interest accrual date is already booked, so add 1 day
|
||||
return add_days(last_posting_date[0][0], 1)
|
||||
last_disbursement_date = get_last_disbursement_date(loan, posting_date)
|
||||
|
||||
if last_disbursement_date and getdate(last_disbursement_date) > getdate(
|
||||
last_interest_accrual_date
|
||||
):
|
||||
last_interest_accrual_date = last_disbursement_date
|
||||
|
||||
return add_days(last_interest_accrual_date, 1)
|
||||
else:
|
||||
return frappe.db.get_value("Loan", loan, "disbursement_date")
|
||||
|
||||
|
||||
def get_last_disbursement_date(loan, posting_date):
|
||||
last_disbursement_date = frappe.db.get_value(
|
||||
"Loan Disbursement",
|
||||
{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
|
||||
"MAX(posting_date)",
|
||||
)
|
||||
|
||||
return last_disbursement_date
|
||||
|
||||
|
||||
def days_in_year(year):
|
||||
days = 365
|
||||
|
||||
|
||||
@@ -101,7 +101,7 @@ class LoanRepayment(AccountsController):
|
||||
if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
|
||||
if not self.is_term_loan:
|
||||
# get last loan interest accrual date
|
||||
last_accrual_date = get_last_accrual_date(self.against_loan)
|
||||
last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date)
|
||||
|
||||
# get posting date upto which interest has to be accrued
|
||||
per_day_interest = get_per_day_interest(
|
||||
@@ -722,7 +722,7 @@ def get_amounts(amounts, against_loan, posting_date):
|
||||
if due_date:
|
||||
pending_days = date_diff(posting_date, due_date) + 1
|
||||
else:
|
||||
last_accrual_date = get_last_accrual_date(against_loan_doc.name)
|
||||
last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date)
|
||||
pending_days = date_diff(posting_date, last_accrual_date) + 1
|
||||
|
||||
if pending_days > 0:
|
||||
|
||||
@@ -7,13 +7,14 @@ from typing import Literal
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils.data import add_to_date, now
|
||||
from frappe.utils.data import add_to_date, now, today
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
JobCardOverTransferError,
|
||||
OperationMismatchError,
|
||||
OverlapError,
|
||||
make_corrective_job_card,
|
||||
make_material_request,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
make_stock_entry as make_stock_entry_from_jc,
|
||||
@@ -449,6 +450,25 @@ class TestJobCard(FrappeTestCase):
|
||||
jc.docstatus = 2
|
||||
assertStatus("Cancelled")
|
||||
|
||||
def test_job_card_material_request_and_bom_details(self):
|
||||
from erpnext.stock.doctype.material_request.material_request import make_stock_entry
|
||||
|
||||
create_bom_with_multiple_operations()
|
||||
work_order = make_wo_with_transfer_against_jc()
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": work_order.name}, "name")
|
||||
|
||||
mr = make_material_request(job_card_name)
|
||||
mr.schedule_date = today()
|
||||
mr.submit()
|
||||
|
||||
ste = make_stock_entry(mr.name)
|
||||
self.assertEqual(ste.purpose, "Material Transfer for Manufacture")
|
||||
self.assertEqual(ste.work_order, work_order.name)
|
||||
self.assertEqual(ste.job_card, job_card_name)
|
||||
self.assertEqual(ste.from_bom, 1.0)
|
||||
self.assertEqual(ste.bom_no, work_order.bom_no)
|
||||
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
"Create a BOM with multiple operations and Material Transfer against Job Card"
|
||||
|
||||
@@ -25,20 +25,38 @@ frappe.listview_settings['Task'] = {
|
||||
}
|
||||
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
|
||||
},
|
||||
gantt_custom_popup_html: function(ganttobj, task) {
|
||||
var html = `<h5><a style="text-decoration:underline"\
|
||||
href="/app/task/${ganttobj.id}""> ${ganttobj.name} </a></h5>`;
|
||||
gantt_custom_popup_html: function (ganttobj, task) {
|
||||
let html = `
|
||||
<a class="text-white mb-2 inline-block cursor-pointer"
|
||||
href="/app/task/${ganttobj.id}"">
|
||||
${ganttobj.name}
|
||||
</a>
|
||||
`;
|
||||
|
||||
if(task.project) html += `<p>Project: ${task.project}</p>`;
|
||||
html += `<p>Progress: ${ganttobj.progress}</p>`;
|
||||
if (task.project) {
|
||||
html += `<p class="mb-1">${__("Project")}:
|
||||
<a class="text-white inline-block"
|
||||
href="/app/project/${task.project}"">
|
||||
${task.project}
|
||||
</a>
|
||||
</p>`;
|
||||
}
|
||||
html += `<p class="mb-1">
|
||||
${__("Progress")}:
|
||||
<span class="text-white">${ganttobj.progress}%</span>
|
||||
</p>`;
|
||||
|
||||
if(task._assign_list) {
|
||||
html += task._assign_list.reduce(
|
||||
(html, user) => html + frappe.avatar(user)
|
||||
, '');
|
||||
if (task._assign) {
|
||||
const assign_list = JSON.parse(task._assign);
|
||||
const assignment_wrapper = `
|
||||
<span>Assigned to:</span>
|
||||
<span class="text-white">
|
||||
${assign_list.map((user) => frappe.user_info(user).fullname).join(", ")}
|
||||
</span>
|
||||
`;
|
||||
html += assignment_wrapper;
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
return `<div class="p-3" style="min-width: 220px">${html}</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -16,8 +16,8 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|
||||
|| (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) {
|
||||
|
||||
let party_type = "Customer";
|
||||
if (frm.doc.quotation_to && frm.doc.quotation_to === "Lead") {
|
||||
party_type = "Lead";
|
||||
if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) {
|
||||
party_type = frm.doc.quotation_to;
|
||||
}
|
||||
|
||||
args = {
|
||||
|
||||
@@ -13,7 +13,7 @@ frappe.ui.form.on('Quotation', {
|
||||
frm.set_query("quotation_to", function() {
|
||||
return{
|
||||
"filters": {
|
||||
"name": ["in", ["Customer", "Lead"]],
|
||||
"name": ["in", ["Customer", "Lead", "Prospect"]],
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -160,19 +160,16 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
|
||||
}
|
||||
|
||||
set_dynamic_field_label(){
|
||||
if (this.frm.doc.quotation_to == "Customer")
|
||||
{
|
||||
if (this.frm.doc.quotation_to == "Customer") {
|
||||
this.frm.set_df_property("party_name", "label", "Customer");
|
||||
this.frm.fields_dict.party_name.get_query = null;
|
||||
}
|
||||
|
||||
if (this.frm.doc.quotation_to == "Lead")
|
||||
{
|
||||
} else if (this.frm.doc.quotation_to == "Lead") {
|
||||
this.frm.set_df_property("party_name", "label", "Lead");
|
||||
|
||||
this.frm.fields_dict.party_name.get_query = function() {
|
||||
return{ query: "erpnext.controllers.queries.lead_query" }
|
||||
}
|
||||
} else if (this.frm.doc.quotation_to == "Prospect") {
|
||||
this.frm.set_df_property("party_name", "label", "Prospect");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -398,10 +398,17 @@ class SalesOrder(SellingController):
|
||||
def update_picking_status(self):
|
||||
total_picked_qty = 0.0
|
||||
total_qty = 0.0
|
||||
per_picked = 0.0
|
||||
|
||||
for so_item in self.items:
|
||||
total_picked_qty += flt(so_item.picked_qty)
|
||||
total_qty += flt(so_item.stock_qty)
|
||||
per_picked = total_picked_qty / total_qty * 100
|
||||
if cint(
|
||||
frappe.get_cached_value("Item", so_item.item_code, "is_stock_item")
|
||||
) or self.has_product_bundle(so_item.item_code):
|
||||
total_picked_qty += flt(so_item.picked_qty)
|
||||
total_qty += flt(so_item.stock_qty)
|
||||
|
||||
if total_picked_qty and total_qty:
|
||||
per_picked = total_picked_qty / total_qty * 100
|
||||
|
||||
self.db_set("per_picked", flt(per_picked), update_modified=False)
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ def after_install():
|
||||
create_default_success_action()
|
||||
create_default_energy_point_rules()
|
||||
create_incoterms()
|
||||
create_default_role_profiles()
|
||||
add_company_to_session_defaults()
|
||||
add_standard_navbar_items()
|
||||
add_app_name()
|
||||
@@ -211,3 +212,16 @@ def setup_log_settings():
|
||||
def hide_workspaces():
|
||||
for ws in ["Integration", "Settings"]:
|
||||
frappe.db.set_value("Workspace", ws, "public", 0)
|
||||
|
||||
|
||||
def create_default_role_profiles():
|
||||
for module in ["Accounts", "Stock", "Manufacturing"]:
|
||||
create_role_profile(module)
|
||||
|
||||
|
||||
def create_role_profile(module):
|
||||
role_profile = frappe.new_doc("Role Profile")
|
||||
role_profile.role_profile = _("{0} User").format(module)
|
||||
role_profile.append("roles", {"role": module + " User"})
|
||||
role_profile.append("roles", {"role": module + " Manager"})
|
||||
role_profile.insert()
|
||||
|
||||
@@ -622,6 +622,16 @@ def make_stock_entry(source_name, target_doc=None):
|
||||
target.stock_entry_type = target.purpose
|
||||
target.set_job_card_data()
|
||||
|
||||
if source.job_card:
|
||||
job_card_details = frappe.get_all(
|
||||
"Job Card", filters={"name": source.job_card}, fields=["bom_no", "for_quantity"]
|
||||
)
|
||||
|
||||
if job_card_details and job_card_details[0]:
|
||||
target.bom_no = job_card_details[0].bom_no
|
||||
target.fg_completed_qty = job_card_details[0].for_quantity
|
||||
target.from_bom = 1
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
"Material Request",
|
||||
source_name,
|
||||
|
||||
@@ -265,6 +265,10 @@ class PickList(Document):
|
||||
for item in locations:
|
||||
if not item.item_code:
|
||||
frappe.throw("Row #{0}: Item Code is Mandatory".format(item.idx))
|
||||
if not cint(
|
||||
frappe.get_cached_value("Item", item.item_code, "is_stock_item")
|
||||
) and not frappe.db.exists("Product Bundle", {"new_item_code": item.item_code}):
|
||||
continue
|
||||
item_code = item.item_code
|
||||
reference = item.sales_order_item or item.material_request_item
|
||||
key = (item_code, item.uom, item.warehouse, item.batch_no, reference)
|
||||
|
||||
@@ -304,6 +304,9 @@ def validate_serial_no(sle, item_det):
|
||||
_("Duplicate Serial No entered for Item {0}").format(sle.item_code), SerialNoDuplicateError
|
||||
)
|
||||
|
||||
allow_existing_serial_no = cint(
|
||||
frappe.get_cached_value("Stock Settings", "None", "allow_existing_serial_no")
|
||||
)
|
||||
for serial_no in serial_nos:
|
||||
if frappe.db.exists("Serial No", serial_no):
|
||||
sr = frappe.db.get_value(
|
||||
@@ -332,6 +335,23 @@ def validate_serial_no(sle, item_det):
|
||||
SerialNoItemError,
|
||||
)
|
||||
|
||||
if not allow_existing_serial_no and sle.voucher_type in [
|
||||
"Stock Entry",
|
||||
"Purchase Receipt",
|
||||
"Purchase Invoice",
|
||||
]:
|
||||
msg = ""
|
||||
|
||||
if sle.voucher_type == "Stock Entry":
|
||||
se_purpose = frappe.db.get_value("Stock Entry", sle.voucher_no, "purpose")
|
||||
if se_purpose in ["Manufacture", "Material Receipt"]:
|
||||
msg = f"Cannot create a {sle.voucher_type} ({se_purpose}) for the Item {frappe.bold(sle.item_code)} with the existing Serial No {frappe.bold(serial_no)}."
|
||||
else:
|
||||
msg = f"Cannot create a {sle.voucher_type} for the Item {frappe.bold(sle.item_code)} with the existing Serial No {frappe.bold(serial_no)}."
|
||||
|
||||
if msg:
|
||||
frappe.throw(_(msg), SerialNoDuplicateError)
|
||||
|
||||
if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
|
||||
doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
|
||||
frappe.throw(
|
||||
|
||||
@@ -751,6 +751,50 @@ class TestStockReconciliation(FrappeTestCase, StockTestMixin):
|
||||
|
||||
self.assertEqual(flt(sle[0].qty_after_transaction), flt(50.0))
|
||||
|
||||
def test_update_stock_reconciliation_while_reposting(self):
|
||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
|
||||
|
||||
item_code = self.make_item().name
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
|
||||
# Stock Value => 100 * 100 = 10000
|
||||
se = make_stock_entry(
|
||||
item_code=item_code,
|
||||
target=warehouse,
|
||||
qty=100,
|
||||
basic_rate=100,
|
||||
posting_time="10:00:00",
|
||||
)
|
||||
|
||||
# Stock Value => 100 * 200 = 20000
|
||||
# Value Change => 20000 - 10000 = 10000
|
||||
sr1 = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=100,
|
||||
rate=200,
|
||||
posting_time="12:00:00",
|
||||
)
|
||||
self.assertEqual(sr1.difference_amount, 10000)
|
||||
|
||||
# Stock Value => 50 * 50 = 2500
|
||||
# Value Change => 2500 - 10000 = -7500
|
||||
sr2 = create_stock_reconciliation(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
qty=50,
|
||||
rate=50,
|
||||
posting_time="11:00:00",
|
||||
)
|
||||
self.assertEqual(sr2.difference_amount, -7500)
|
||||
|
||||
sr1.load_from_db()
|
||||
self.assertEqual(sr1.difference_amount, 17500)
|
||||
|
||||
sr2.cancel()
|
||||
sr1.load_from_db()
|
||||
self.assertEqual(sr1.difference_amount, 10000)
|
||||
|
||||
|
||||
def create_batch_item_with_batch(item_name, batch_id):
|
||||
batch_item_doc = create_item(item_name, is_stock_item=1)
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
"section_break_7",
|
||||
"automatically_set_serial_nos_based_on_fifo",
|
||||
"set_qty_in_transactions_based_on_serial_no_input",
|
||||
"allow_existing_serial_no",
|
||||
"column_break_10",
|
||||
"disable_serial_no_and_batch_selector",
|
||||
"use_naming_series",
|
||||
@@ -340,6 +341,12 @@
|
||||
{
|
||||
"fieldname": "column_break_121",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "allow_existing_serial_no",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow existing Serial No to be Manufactured/Received again"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@@ -347,7 +354,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-05-29 15:09:54.959411",
|
||||
"modified": "2023-05-31 14:15:14.145048",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
||||
@@ -53,11 +53,14 @@ frappe.query_reports["Stock and Account Value Comparison"] = {
|
||||
<p>Are you sure you want to create Reposting Entries?</p>
|
||||
</div>
|
||||
`;
|
||||
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
|
||||
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
|
||||
|
||||
if (!selected_rows.length) {
|
||||
frappe.throw(__("Please select rows to create Reposting Entries"));
|
||||
}
|
||||
|
||||
frappe.confirm(__(message), () => {
|
||||
let indexes = frappe.query_report.datatable.rowmanager.getCheckedRows();
|
||||
let selected_rows = indexes.map(i => frappe.query_report.data[i]);
|
||||
|
||||
frappe.call({
|
||||
method: "erpnext.stock.report.stock_and_account_value_comparison.stock_and_account_value_comparison.create_reposting_entries",
|
||||
args: {
|
||||
|
||||
@@ -728,6 +728,8 @@ class update_entries_after(object):
|
||||
self.update_rate_on_purchase_receipt(sle, outgoing_rate)
|
||||
elif flt(sle.actual_qty) < 0 and sle.voucher_type == "Subcontracting Receipt":
|
||||
self.update_rate_on_subcontracting_receipt(sle, outgoing_rate)
|
||||
elif sle.voucher_type == "Stock Reconciliation":
|
||||
self.update_rate_on_stock_reconciliation(sle)
|
||||
|
||||
def update_rate_on_stock_entry(self, sle, outgoing_rate):
|
||||
frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
|
||||
@@ -795,6 +797,38 @@ class update_entries_after(object):
|
||||
for d in scr.items:
|
||||
d.db_update()
|
||||
|
||||
def update_rate_on_stock_reconciliation(self, sle):
|
||||
if not sle.serial_no and not sle.batch_no:
|
||||
sr = frappe.get_doc("Stock Reconciliation", sle.voucher_no, for_update=True)
|
||||
|
||||
for item in sr.items:
|
||||
# Skip for Serial and Batch Items
|
||||
if item.serial_no or item.batch_no:
|
||||
continue
|
||||
|
||||
previous_sle = get_previous_sle(
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"warehouse": item.warehouse,
|
||||
"posting_date": sr.posting_date,
|
||||
"posting_time": sr.posting_time,
|
||||
"sle": sle.name,
|
||||
}
|
||||
)
|
||||
|
||||
item.current_qty = previous_sle.get("qty_after_transaction") or 0.0
|
||||
item.current_valuation_rate = previous_sle.get("valuation_rate") or 0.0
|
||||
item.current_amount = flt(item.current_qty) * flt(item.current_valuation_rate)
|
||||
|
||||
item.amount = flt(item.qty) * flt(item.valuation_rate)
|
||||
item.amount_difference = item.amount - item.current_amount
|
||||
else:
|
||||
sr.difference_amount = sum([item.amount_difference for item in sr.items])
|
||||
sr.db_update()
|
||||
|
||||
for item in sr.items:
|
||||
item.db_update()
|
||||
|
||||
def get_serialized_values(self, sle):
|
||||
incoming_rate = flt(sle.incoming_rate)
|
||||
actual_qty = flt(sle.actual_qty)
|
||||
|
||||
Reference in New Issue
Block a user