mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-25 13:03:02 +00:00
Merge branch 'develop' into version-15-beta
This commit is contained in:
@@ -702,7 +702,50 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe2.submit()
|
||||
|
||||
# create return entry against si1
|
||||
create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
|
||||
cr_note = create_sales_invoice(is_return=1, return_against=si1.name, qty=-1)
|
||||
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
||||
|
||||
# create JE(credit note) manually against si1 and cr_note
|
||||
je = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Journal Entry",
|
||||
"company": si1.company,
|
||||
"voucher_type": "Credit Note",
|
||||
"posting_date": nowdate(),
|
||||
}
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": si1.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si1.customer,
|
||||
"debit": 0,
|
||||
"credit": 100,
|
||||
"debit_in_account_currency": 0,
|
||||
"credit_in_account_currency": 100,
|
||||
"reference_type": si1.doctype,
|
||||
"reference_name": si1.name,
|
||||
"cost_center": si1.items[0].cost_center,
|
||||
},
|
||||
)
|
||||
je.append(
|
||||
"accounts",
|
||||
{
|
||||
"account": cr_note.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": cr_note.customer,
|
||||
"debit": 100,
|
||||
"credit": 0,
|
||||
"debit_in_account_currency": 100,
|
||||
"credit_in_account_currency": 0,
|
||||
"reference_type": cr_note.doctype,
|
||||
"reference_name": cr_note.name,
|
||||
"cost_center": cr_note.items[0].cost_center,
|
||||
},
|
||||
)
|
||||
je.save().submit()
|
||||
|
||||
si1_outstanding = frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount")
|
||||
self.assertEqual(si1_outstanding, -100)
|
||||
|
||||
|
||||
@@ -294,7 +294,7 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
cr_note1.return_against = si3.name
|
||||
cr_note1 = cr_note1.save().submit()
|
||||
|
||||
pl_entries = (
|
||||
pl_entries_si3 = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
@@ -309,7 +309,24 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values = [
|
||||
pl_entries_cr_note1 = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.against_voucher_type,
|
||||
ple.against_voucher_no,
|
||||
ple.amount,
|
||||
ple.delinked,
|
||||
)
|
||||
.where(
|
||||
(ple.against_voucher_type == cr_note1.doctype) & (ple.against_voucher_no == cr_note1.name)
|
||||
)
|
||||
.orderby(ple.creation)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
expected_values_for_si3 = [
|
||||
{
|
||||
"voucher_type": si3.doctype,
|
||||
"voucher_no": si3.name,
|
||||
@@ -317,18 +334,21 @@ class TestPaymentLedgerEntry(FrappeTestCase):
|
||||
"against_voucher_no": si3.name,
|
||||
"amount": amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
}
|
||||
]
|
||||
# credit/debit notes post ledger entries against itself
|
||||
expected_values_for_cr_note1 = [
|
||||
{
|
||||
"voucher_type": cr_note1.doctype,
|
||||
"voucher_no": cr_note1.name,
|
||||
"against_voucher_type": si3.doctype,
|
||||
"against_voucher_no": si3.name,
|
||||
"against_voucher_type": cr_note1.doctype,
|
||||
"against_voucher_no": cr_note1.name,
|
||||
"amount": -amount,
|
||||
"delinked": 0,
|
||||
},
|
||||
]
|
||||
self.assertEqual(pl_entries[0], expected_values[0])
|
||||
self.assertEqual(pl_entries[1], expected_values[1])
|
||||
self.assertEqual(pl_entries_si3, expected_values_for_si3)
|
||||
self.assertEqual(pl_entries_cr_note1, expected_values_for_cr_note1)
|
||||
|
||||
def test_je_against_inv_and_note(self):
|
||||
ple = self.ple
|
||||
|
||||
@@ -163,6 +163,15 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
this.frm.refresh();
|
||||
}
|
||||
|
||||
invoice_name() {
|
||||
this.frm.trigger("get_unreconciled_entries");
|
||||
}
|
||||
|
||||
payment_name() {
|
||||
this.frm.trigger("get_unreconciled_entries");
|
||||
}
|
||||
|
||||
|
||||
clear_child_tables() {
|
||||
this.frm.clear_table("invoices");
|
||||
this.frm.clear_table("payments");
|
||||
|
||||
@@ -27,8 +27,10 @@
|
||||
"bank_cash_account",
|
||||
"cost_center",
|
||||
"sec_break1",
|
||||
"invoice_name",
|
||||
"invoices",
|
||||
"column_break_15",
|
||||
"payment_name",
|
||||
"payments",
|
||||
"sec_break2",
|
||||
"allocation"
|
||||
@@ -137,6 +139,7 @@
|
||||
"label": "Minimum Invoice Amount"
|
||||
},
|
||||
{
|
||||
"default": "50",
|
||||
"description": "System will fetch all the entries if limit value is zero.",
|
||||
"fieldname": "invoice_limit",
|
||||
"fieldtype": "Int",
|
||||
@@ -167,6 +170,7 @@
|
||||
"label": "Maximum Payment Amount"
|
||||
},
|
||||
{
|
||||
"default": "50",
|
||||
"description": "System will fetch all the entries if limit value is zero.",
|
||||
"fieldname": "payment_limit",
|
||||
"fieldtype": "Int",
|
||||
@@ -194,13 +198,23 @@
|
||||
"label": "Default Advance Account",
|
||||
"mandatory_depends_on": "doc.party_type",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filter on Invoice"
|
||||
},
|
||||
{
|
||||
"fieldname": "payment_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Filter on Payment"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-09 13:02:48.718362",
|
||||
"modified": "2023-08-15 05:35:50.109290",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Criterion
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.utils import flt, fmt_money, get_link_to_form, getdate, nowdate, today
|
||||
|
||||
@@ -74,6 +75,9 @@ class PaymentReconciliation(Document):
|
||||
}
|
||||
)
|
||||
|
||||
if self.payment_name:
|
||||
condition.update({"name": self.payment_name})
|
||||
|
||||
payment_entries = get_advance_payment_entries(
|
||||
self.party_type,
|
||||
self.party,
|
||||
@@ -89,6 +93,9 @@ class PaymentReconciliation(Document):
|
||||
def get_jv_entries(self):
|
||||
condition = self.get_conditions()
|
||||
|
||||
if self.payment_name:
|
||||
condition += f" and t1.name like '%%{self.payment_name}%%'"
|
||||
|
||||
if self.get("cost_center"):
|
||||
condition += f" and t2.cost_center = '{self.cost_center}' "
|
||||
|
||||
@@ -146,6 +153,15 @@ class PaymentReconciliation(Document):
|
||||
def get_return_invoices(self):
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
doc = qb.DocType(voucher_type)
|
||||
|
||||
conditions = []
|
||||
conditions.append(doc.docstatus == 1)
|
||||
conditions.append(doc[frappe.scrub(self.party_type)] == self.party)
|
||||
conditions.append(doc.is_return == 1)
|
||||
|
||||
if self.payment_name:
|
||||
conditions.append(doc.name.like(f"%{self.payment_name}%"))
|
||||
|
||||
self.return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(
|
||||
@@ -153,11 +169,7 @@ class PaymentReconciliation(Document):
|
||||
doc.name.as_("voucher_no"),
|
||||
doc.return_against,
|
||||
)
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
)
|
||||
.where(Criterion.all(conditions))
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
@@ -174,15 +186,12 @@ class PaymentReconciliation(Document):
|
||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||
|
||||
self.get_return_invoices()
|
||||
return_invoices = [
|
||||
x for x in self.return_invoices if x.return_against == None or x.return_against == ""
|
||||
]
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
if self.return_invoices:
|
||||
ple_query = QueryPaymentLedger()
|
||||
return_outstanding = ple_query.get_voucher_outstandings(
|
||||
vouchers=return_invoices,
|
||||
vouchers=self.return_invoices,
|
||||
common_filter=self.common_filter_conditions,
|
||||
posting_date=self.ple_posting_date_filter,
|
||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||
@@ -226,6 +235,8 @@ class PaymentReconciliation(Document):
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
||||
accounting_dimensions=self.accounting_dimension_filter_conditions,
|
||||
limit=self.invoice_limit,
|
||||
voucher_no=self.invoice_name,
|
||||
)
|
||||
|
||||
cr_dr_notes = (
|
||||
|
||||
@@ -86,8 +86,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
||||
}
|
||||
}
|
||||
|
||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0
|
||||
&& !(doc.is_return && doc.return_against) && !doc.on_hold) {
|
||||
if(doc.docstatus == 1 && doc.outstanding_amount != 0 && !doc.on_hold) {
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
|
||||
@@ -628,9 +628,7 @@ class PurchaseInvoice(BuyingController):
|
||||
"credit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"project": self.project,
|
||||
"cost_center": self.cost_center,
|
||||
@@ -1644,12 +1642,8 @@ class PurchaseInvoice(BuyingController):
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
# Check if outstanding amount is 0 due to debit note issued against invoice
|
||||
elif (
|
||||
outstanding_amount <= 0
|
||||
and self.is_return == 0
|
||||
and frappe.db.get_value(
|
||||
"Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||
)
|
||||
elif self.is_return == 0 and frappe.db.get_value(
|
||||
"Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||
):
|
||||
self.status = "Debit Note Issued"
|
||||
elif self.is_return == 1:
|
||||
|
||||
@@ -98,8 +98,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
erpnext.accounts.ledger_preview.show_stock_ledger_preview(this.frm);
|
||||
}
|
||||
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0
|
||||
&& !(cint(doc.is_return) && doc.return_against)) {
|
||||
if (doc.docstatus == 1 && doc.outstanding_amount!=0) {
|
||||
this.frm.add_custom_button(
|
||||
__('Payment'),
|
||||
() => this.make_payment_entry(),
|
||||
|
||||
@@ -1104,9 +1104,7 @@ class SalesInvoice(SellingController):
|
||||
"debit_in_account_currency": base_grand_total
|
||||
if self.party_account_currency == self.company_currency
|
||||
else grand_total,
|
||||
"against_voucher": self.return_against
|
||||
if cint(self.is_return) and self.return_against
|
||||
else self.name,
|
||||
"against_voucher": self.name,
|
||||
"against_voucher_type": self.doctype,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
@@ -1732,12 +1730,8 @@ class SalesInvoice(SellingController):
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
# Check if outstanding amount is 0 due to credit note issued against invoice
|
||||
elif (
|
||||
outstanding_amount <= 0
|
||||
and self.is_return == 0
|
||||
and frappe.db.get_value(
|
||||
"Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||
)
|
||||
elif self.is_return == 0 and frappe.db.get_value(
|
||||
"Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1}
|
||||
):
|
||||
self.status = "Credit Note Issued"
|
||||
elif self.is_return == 1:
|
||||
|
||||
@@ -1500,8 +1500,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(party_credited, 1000)
|
||||
|
||||
# Check outstanding amount
|
||||
self.assertFalse(si1.outstanding_amount)
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 1500)
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si1.name, "outstanding_amount"), -1000)
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"), 2500)
|
||||
|
||||
def test_gle_made_when_asset_is_returned(self):
|
||||
create_asset_data()
|
||||
|
||||
@@ -214,8 +214,8 @@ class ReceivablePayableReport(object):
|
||||
for party_type in self.party_type:
|
||||
if self.filters.get(scrub(party_type)):
|
||||
amount = ple.amount_in_account_currency
|
||||
else:
|
||||
amount = ple.amount
|
||||
else:
|
||||
amount = ple.amount
|
||||
amount_in_account_currency = ple.amount_in_account_currency
|
||||
|
||||
# update voucher
|
||||
|
||||
@@ -908,7 +908,9 @@ def get_outstanding_invoices(
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
accounting_dimensions=None,
|
||||
vouchers=None,
|
||||
vouchers=None, # list of dicts [{'voucher_type': '', 'voucher_no': ''}] for filtering
|
||||
limit=None, # passed by reconciliation tool
|
||||
voucher_no=None, # filter passed by reconciliation tool
|
||||
):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@@ -941,6 +943,8 @@ def get_outstanding_invoices(
|
||||
max_outstanding=max_outstanding,
|
||||
get_invoices=True,
|
||||
accounting_dimensions=accounting_dimensions or [],
|
||||
limit=limit,
|
||||
voucher_no=voucher_no,
|
||||
)
|
||||
|
||||
for d in invoice_list:
|
||||
@@ -1678,12 +1682,13 @@ class QueryPaymentLedger(object):
|
||||
self.voucher_posting_date = []
|
||||
self.min_outstanding = None
|
||||
self.max_outstanding = None
|
||||
self.limit = self.voucher_no = None
|
||||
|
||||
def reset(self):
|
||||
# clear filters
|
||||
self.vouchers.clear()
|
||||
self.common_filter.clear()
|
||||
self.min_outstanding = self.max_outstanding = None
|
||||
self.min_outstanding = self.max_outstanding = self.limit = None
|
||||
|
||||
# clear result
|
||||
self.voucher_outstandings.clear()
|
||||
@@ -1697,6 +1702,7 @@ class QueryPaymentLedger(object):
|
||||
|
||||
filter_on_voucher_no = []
|
||||
filter_on_against_voucher_no = []
|
||||
|
||||
if self.vouchers:
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
voucher_nos = set([x.voucher_no for x in self.vouchers])
|
||||
@@ -1707,6 +1713,10 @@ class QueryPaymentLedger(object):
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
|
||||
|
||||
if self.voucher_no:
|
||||
filter_on_voucher_no.append(ple.voucher_no.like(f"%{self.voucher_no}%"))
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_no.like(f"%{self.voucher_no}%"))
|
||||
|
||||
# build outstanding amount filter
|
||||
filter_on_outstanding_amount = []
|
||||
if self.min_outstanding:
|
||||
@@ -1822,6 +1832,11 @@ class QueryPaymentLedger(object):
|
||||
)
|
||||
)
|
||||
|
||||
if self.limit:
|
||||
self.cte_query_voucher_amount_and_outstanding = (
|
||||
self.cte_query_voucher_amount_and_outstanding.limit(self.limit)
|
||||
)
|
||||
|
||||
# execute SQL
|
||||
self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
|
||||
|
||||
@@ -1835,6 +1850,8 @@ class QueryPaymentLedger(object):
|
||||
get_payments=False,
|
||||
get_invoices=False,
|
||||
accounting_dimensions=None,
|
||||
limit=None,
|
||||
voucher_no=None,
|
||||
):
|
||||
"""
|
||||
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
|
||||
@@ -1856,6 +1873,8 @@ class QueryPaymentLedger(object):
|
||||
self.max_outstanding = max_outstanding
|
||||
self.get_payments = get_payments
|
||||
self.get_invoices = get_invoices
|
||||
self.limit = limit
|
||||
self.voucher_no = voucher_no
|
||||
self.query_for_outstanding()
|
||||
|
||||
return self.voucher_outstandings
|
||||
|
||||
@@ -228,15 +228,19 @@ frappe.ui.form.on('Asset', {
|
||||
{name: __("Schedule Date"), editable: false, resizable: false, width: 270},
|
||||
{name: __("Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||
{name: __("Accumulated Depreciation Amount"), editable: false, resizable: false, width: 164},
|
||||
{name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 312}
|
||||
{name: __("Journal Entry"), editable: false, resizable: false, format: value => `<a href="/app/journal-entry/${value}">${value}</a>`, width: 304}
|
||||
],
|
||||
data: data,
|
||||
layout: "fluid",
|
||||
serialNoColumn: false,
|
||||
checkboxColumn: true,
|
||||
cellHeight: 35
|
||||
});
|
||||
|
||||
datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem'});
|
||||
datatable.style.setStyle(`.dt-scrollable`, {'font-size': '0.75rem', 'margin-bottom': '1rem', 'margin-left': '0.35rem', 'margin-right': '0.35rem'});
|
||||
datatable.style.setStyle(`.dt-header`, {'margin-left': '0.35rem', 'margin-right': '0.35rem'});
|
||||
datatable.style.setStyle(`.dt-cell--header`, {'color': 'var(--text-muted)'});
|
||||
datatable.style.setStyle(`.dt-cell`, {'color': 'var(--text-color)'});
|
||||
datatable.style.setStyle(`.dt-cell--col-1`, {'text-align': 'center'});
|
||||
datatable.style.setStyle(`.dt-cell--col-2`, {'font-weight': 600});
|
||||
datatable.style.setStyle(`.dt-cell--col-3`, {'font-weight': 600});
|
||||
|
||||
@@ -96,11 +96,14 @@ class Asset(AccountsController):
|
||||
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
|
||||
).format(asset_depr_schedules_links)
|
||||
)
|
||||
if not frappe.db.exists(
|
||||
{
|
||||
"doctype": "Asset Activity",
|
||||
"asset": self.name,
|
||||
}
|
||||
if (
|
||||
not frappe.db.exists(
|
||||
{
|
||||
"doctype": "Asset Activity",
|
||||
"asset": self.name,
|
||||
}
|
||||
)
|
||||
and not self.flags.asset_created_via_asset_capitalization
|
||||
):
|
||||
add_asset_activity(self.name, _("Asset created"))
|
||||
|
||||
|
||||
@@ -509,6 +509,7 @@ class AssetCapitalization(StockController):
|
||||
asset_doc.gross_purchase_amount = total_target_asset_value
|
||||
asset_doc.purchase_receipt_amount = total_target_asset_value
|
||||
asset_doc.flags.ignore_validate = True
|
||||
asset_doc.flags.asset_created_via_asset_capitalization = True
|
||||
asset_doc.insert()
|
||||
|
||||
self.target_asset = asset_doc.name
|
||||
|
||||
@@ -779,9 +779,20 @@ def make_new_active_asset_depr_schedules_and_cancel_current_ones(
|
||||
def get_temp_asset_depr_schedule_doc(
|
||||
asset_doc, row, date_of_disposal=None, date_of_return=None, update_asset_finance_book_row=False
|
||||
):
|
||||
asset_depr_schedule_doc = frappe.new_doc("Asset Depreciation Schedule")
|
||||
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
|
||||
asset_doc.name, "Active", row.finance_book
|
||||
)
|
||||
|
||||
asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
|
||||
if not current_asset_depr_schedule_doc:
|
||||
frappe.throw(
|
||||
_("Asset Depreciation Schedule not found for Asset {0} and Finance Book {1}").format(
|
||||
asset_doc.name, row.finance_book
|
||||
)
|
||||
)
|
||||
|
||||
temp_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
|
||||
|
||||
temp_asset_depr_schedule_doc.prepare_draft_asset_depr_schedule_data(
|
||||
asset_doc,
|
||||
row,
|
||||
date_of_disposal,
|
||||
@@ -789,7 +800,7 @@ def get_temp_asset_depr_schedule_doc(
|
||||
update_asset_finance_book_row,
|
||||
)
|
||||
|
||||
return asset_depr_schedule_doc
|
||||
return temp_asset_depr_schedule_doc
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
||||
@@ -185,8 +185,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
|
||||
if(!in_list(["Closed", "Delivered"], doc.status)) {
|
||||
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received, 2) < 100 && flt(this.frm.doc.per_billed, 2) < 100) {
|
||||
// Don't add Update Items button if the PO is following the new subcontracting flow.
|
||||
if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
|
||||
if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
|
||||
this.frm.add_custom_button(__('Update Items'), () => {
|
||||
erpnext.utils.update_child_items({
|
||||
frm: this.frm,
|
||||
|
||||
@@ -52,6 +52,7 @@ class PurchaseOrder(BuyingController):
|
||||
def onload(self):
|
||||
supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
|
||||
self.set_onload("supplier_tds", supplier_tds)
|
||||
self.set_onload("can_update_items", self.can_update_items())
|
||||
|
||||
def validate(self):
|
||||
super(PurchaseOrder, self).validate()
|
||||
@@ -450,6 +451,17 @@ class PurchaseOrder(BuyingController):
|
||||
else:
|
||||
self.db_set("per_received", 0, update_modified=False)
|
||||
|
||||
def can_update_items(self) -> bool:
|
||||
result = True
|
||||
|
||||
if self.is_subcontracted and not self.is_old_subcontracting_flow:
|
||||
if frappe.db.exists(
|
||||
"Subcontracting Order", {"purchase_order": self.name, "docstatus": ["!=", 2]}
|
||||
):
|
||||
result = False
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor=1.0):
|
||||
"""get last purchase rate for an item"""
|
||||
|
||||
@@ -901,6 +901,71 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
|
||||
self.assertRaises(frappe.ValidationError, po.save)
|
||||
|
||||
def test_update_items_for_subcontracting_purchase_order(self):
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
get_subcontracting_order,
|
||||
make_bom_for_subcontracted_items,
|
||||
make_raw_materials,
|
||||
make_service_items,
|
||||
make_subcontracted_items,
|
||||
)
|
||||
|
||||
def update_items(po, qty):
|
||||
trans_items = [po.items[0].as_dict()]
|
||||
trans_items[0]["qty"] = qty
|
||||
trans_items[0]["fg_item_qty"] = qty
|
||||
trans_items = json.dumps(trans_items, default=str)
|
||||
|
||||
return update_child_qty_rate(
|
||||
po.doctype,
|
||||
trans_items,
|
||||
po.name,
|
||||
)
|
||||
|
||||
make_subcontracted_items()
|
||||
make_raw_materials()
|
||||
make_service_items()
|
||||
make_bom_for_subcontracted_items()
|
||||
|
||||
service_items = [
|
||||
{
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"item_code": "Subcontracted Service Item 7",
|
||||
"qty": 10,
|
||||
"rate": 100,
|
||||
"fg_item": "Subcontracted Item SA7",
|
||||
"fg_item_qty": 10,
|
||||
},
|
||||
]
|
||||
po = create_purchase_order(
|
||||
rm_items=service_items,
|
||||
is_subcontracted=1,
|
||||
supplier_warehouse="_Test Warehouse 1 - _TC",
|
||||
)
|
||||
|
||||
update_items(po, qty=20)
|
||||
po.reload()
|
||||
|
||||
# Test - 1: Items should be updated as there is no Subcontracting Order against PO
|
||||
self.assertEqual(po.items[0].qty, 20)
|
||||
self.assertEqual(po.items[0].fg_item_qty, 20)
|
||||
|
||||
sco = get_subcontracting_order(po_name=po.name, warehouse="_Test Warehouse - _TC")
|
||||
|
||||
# Test - 2: ValidationError should be raised as there is Subcontracting Order against PO
|
||||
self.assertRaises(frappe.ValidationError, update_items, po=po, qty=30)
|
||||
|
||||
sco.reload()
|
||||
sco.cancel()
|
||||
po.reload()
|
||||
|
||||
update_items(po, qty=30)
|
||||
po.reload()
|
||||
|
||||
# Test - 3: Items should be updated as the Subcontracting Order is cancelled
|
||||
self.assertEqual(po.items[0].qty, 30)
|
||||
self.assertEqual(po.items[0].fg_item_qty, 30)
|
||||
|
||||
|
||||
def prepare_data_for_internal_transfer():
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier
|
||||
|
||||
@@ -154,31 +154,35 @@ def get_data(filters):
|
||||
procurement_record = []
|
||||
if procurement_record_against_mr:
|
||||
procurement_record += procurement_record_against_mr
|
||||
|
||||
for po in purchase_order_entry:
|
||||
# fetch material records linked to the purchase order item
|
||||
mr_record = mr_records.get(po.material_request_item, [{}])[0]
|
||||
procurement_detail = {
|
||||
"material_request_date": mr_record.get("transaction_date"),
|
||||
"cost_center": po.cost_center,
|
||||
"project": po.project,
|
||||
"requesting_site": po.warehouse,
|
||||
"requestor": po.owner,
|
||||
"material_request_no": po.material_request,
|
||||
"item_code": po.item_code,
|
||||
"quantity": flt(po.qty),
|
||||
"unit_of_measurement": po.stock_uom,
|
||||
"status": po.status,
|
||||
"purchase_order_date": po.transaction_date,
|
||||
"purchase_order": po.parent,
|
||||
"supplier": po.supplier,
|
||||
"estimated_cost": flt(mr_record.get("amount")),
|
||||
"actual_cost": flt(pi_records.get(po.name)),
|
||||
"purchase_order_amt": flt(po.amount),
|
||||
"purchase_order_amt_in_company_currency": flt(po.base_amount),
|
||||
"expected_delivery_date": po.schedule_date,
|
||||
"actual_delivery_date": pr_records.get(po.name),
|
||||
}
|
||||
procurement_record.append(procurement_detail)
|
||||
material_requests = mr_records.get(po.material_request_item, [{}])
|
||||
|
||||
for mr_record in material_requests:
|
||||
procurement_detail = {
|
||||
"material_request_date": mr_record.get("transaction_date"),
|
||||
"cost_center": po.cost_center,
|
||||
"project": po.project,
|
||||
"requesting_site": po.warehouse,
|
||||
"requestor": po.owner,
|
||||
"material_request_no": po.material_request,
|
||||
"item_code": po.item_code,
|
||||
"quantity": flt(po.qty),
|
||||
"unit_of_measurement": po.stock_uom,
|
||||
"status": po.status,
|
||||
"purchase_order_date": po.transaction_date,
|
||||
"purchase_order": po.parent,
|
||||
"supplier": po.supplier,
|
||||
"estimated_cost": flt(mr_record.get("amount")),
|
||||
"actual_cost": flt(pi_records.get(po.name)),
|
||||
"purchase_order_amt": flt(po.amount),
|
||||
"purchase_order_amt_in_company_currency": flt(po.base_amount),
|
||||
"expected_delivery_date": po.schedule_date,
|
||||
"actual_delivery_date": pr_records.get(po.name),
|
||||
}
|
||||
procurement_record.append(procurement_detail)
|
||||
|
||||
return procurement_record
|
||||
|
||||
|
||||
@@ -301,7 +305,7 @@ def get_po_entries(filters):
|
||||
& (parent.name == child.parent)
|
||||
& (parent.status.notin(("Closed", "Completed", "Cancelled")))
|
||||
)
|
||||
.groupby(parent.name, child.item_code)
|
||||
.groupby(parent.name, child.material_request_item)
|
||||
)
|
||||
query = apply_filters_on_query(filters, parent, child, query)
|
||||
|
||||
|
||||
@@ -2418,6 +2418,9 @@ def get_common_query(
|
||||
q = q.select((payment_entry.target_exchange_rate).as_("exchange_rate"))
|
||||
|
||||
if condition:
|
||||
if condition.get("name", None):
|
||||
q = q.where(payment_entry.name.like(f"%{condition.get('name')}%"))
|
||||
|
||||
q = q.where(payment_entry.company == condition["company"])
|
||||
q = (
|
||||
q.where(payment_entry.posting_date >= condition["from_payment_date"])
|
||||
@@ -2855,6 +2858,27 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
return update_supplied_items
|
||||
|
||||
def validate_fg_item_for_subcontracting(new_data, is_new):
|
||||
if is_new:
|
||||
if not new_data.get("fg_item"):
|
||||
frappe.throw(
|
||||
_("Finished Good Item is not specified for service item {0}").format(new_data["item_code"])
|
||||
)
|
||||
else:
|
||||
is_sub_contracted_item, default_bom = frappe.db.get_value(
|
||||
"Item", new_data["fg_item"], ["is_sub_contracted_item", "default_bom"]
|
||||
)
|
||||
|
||||
if not is_sub_contracted_item:
|
||||
frappe.throw(
|
||||
_("Finished Good Item {0} must be a sub-contracted item").format(new_data["fg_item"])
|
||||
)
|
||||
elif not default_bom:
|
||||
frappe.throw(_("Default BOM not found for FG Item {0}").format(new_data["fg_item"]))
|
||||
|
||||
if not new_data.get("fg_item_qty"):
|
||||
frappe.throw(_("Finished Good Item {0} Qty can not be zero").format(new_data["fg_item"]))
|
||||
|
||||
data = json.loads(trans_items)
|
||||
|
||||
any_qty_changed = False # updated to true if any item's qty changes
|
||||
@@ -2886,6 +2910,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
prev_rate, new_rate = flt(child_item.get("rate")), flt(d.get("rate"))
|
||||
prev_qty, new_qty = flt(child_item.get("qty")), flt(d.get("qty"))
|
||||
prev_fg_qty, new_fg_qty = flt(child_item.get("fg_item_qty")), flt(d.get("fg_item_qty"))
|
||||
prev_con_fac, new_con_fac = flt(child_item.get("conversion_factor")), flt(
|
||||
d.get("conversion_factor")
|
||||
)
|
||||
@@ -2898,6 +2923,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
rate_unchanged = prev_rate == new_rate
|
||||
qty_unchanged = prev_qty == new_qty
|
||||
fg_qty_unchanged = prev_fg_qty == new_fg_qty
|
||||
uom_unchanged = prev_uom == new_uom
|
||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||
@@ -2907,6 +2933,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
if (
|
||||
rate_unchanged
|
||||
and qty_unchanged
|
||||
and fg_qty_unchanged
|
||||
and conversion_factor_unchanged
|
||||
and uom_unchanged
|
||||
and date_unchanged
|
||||
@@ -2917,6 +2944,17 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
if (
|
||||
parent.doctype == "Purchase Order"
|
||||
and parent.is_subcontracted
|
||||
and not parent.is_old_subcontracting_flow
|
||||
):
|
||||
validate_fg_item_for_subcontracting(d, new_child_flag)
|
||||
child_item.fg_item_qty = flt(d["fg_item_qty"])
|
||||
|
||||
if new_child_flag:
|
||||
child_item.fg_item = d["fg_item"]
|
||||
|
||||
child_item.qty = flt(d.get("qty"))
|
||||
rate_precision = child_item.precision("rate") or 2
|
||||
conv_fac_precision = child_item.precision("conversion_factor") or 2
|
||||
@@ -3020,11 +3058,20 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_ordered_qty()
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
if parent.is_old_subcontracting_flow:
|
||||
if should_update_supplied_items(parent):
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied()
|
||||
parent.save()
|
||||
|
||||
if parent.is_subcontracted:
|
||||
if parent.is_old_subcontracting_flow:
|
||||
if should_update_supplied_items(parent):
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied()
|
||||
parent.save()
|
||||
else:
|
||||
if not parent.can_update_items():
|
||||
frappe.throw(
|
||||
_(
|
||||
"Items cannot be updated as Subcontracting Order is created against the Purchase Order {0}."
|
||||
).format(frappe.bold(parent.name))
|
||||
)
|
||||
else: # Sales Order
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
|
||||
@@ -1090,7 +1090,7 @@ def get_subcontracting_order(**args):
|
||||
po = frappe.get_doc("Purchase Order", args.get("po_name"))
|
||||
|
||||
if po.is_subcontracted:
|
||||
return create_subcontracting_order(po_name=po.name, **args)
|
||||
return create_subcontracting_order(**args)
|
||||
|
||||
if not args.service_items:
|
||||
service_items = [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
frappe.provide("erpnext.crm");
|
||||
erpnext.pre_sales.set_as_lost("Quotation");
|
||||
erpnext.pre_sales.set_as_lost("Opportunity");
|
||||
erpnext.sales_common.setup_selling_controller();
|
||||
|
||||
|
||||
|
||||
@@ -579,7 +579,9 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
"conversion_factor": d.conversion_factor,
|
||||
"qty": d.qty,
|
||||
"rate": d.rate,
|
||||
"uom": d.uom
|
||||
"uom": d.uom,
|
||||
"fg_item": d.fg_item,
|
||||
"fg_item_qty": d.fg_item_qty,
|
||||
}
|
||||
});
|
||||
|
||||
@@ -678,6 +680,37 @@ erpnext.utils.update_child_items = function(opts) {
|
||||
})
|
||||
}
|
||||
|
||||
if (frm.doc.doctype == 'Purchase Order' && frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
|
||||
fields.push({
|
||||
fieldtype:'Link',
|
||||
fieldname:'fg_item',
|
||||
options: 'Item',
|
||||
reqd: 1,
|
||||
in_list_view: 0,
|
||||
read_only: 0,
|
||||
disabled: 0,
|
||||
label: __('Finished Good Item'),
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
'is_stock_item': 1,
|
||||
'is_sub_contracted_item': 1,
|
||||
'default_bom': ['!=', '']
|
||||
}
|
||||
}
|
||||
},
|
||||
}, {
|
||||
fieldtype:'Float',
|
||||
fieldname:'fg_item_qty',
|
||||
reqd: 1,
|
||||
default: 0,
|
||||
read_only: 0,
|
||||
in_list_view: 0,
|
||||
label: __('Finished Good Item Qty'),
|
||||
precision: get_precision('fg_item_qty')
|
||||
})
|
||||
}
|
||||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Update Items"),
|
||||
size: "extra-large",
|
||||
|
||||
@@ -656,7 +656,10 @@ def make_stock_entry(source_name, target_doc=None):
|
||||
"job_card_item": "job_card_item",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.ordered_qty < doc.stock_qty,
|
||||
"condition": lambda doc: (
|
||||
flt(doc.ordered_qty, doc.precision("ordered_qty"))
|
||||
< flt(doc.stock_qty, doc.precision("ordered_qty"))
|
||||
),
|
||||
},
|
||||
},
|
||||
target_doc,
|
||||
|
||||
Reference in New Issue
Block a user