Merge branch 'develop' into version-15-beta

This commit is contained in:
Ankush Menat
2023-08-23 21:00:55 +05:30
25 changed files with 376 additions and 92 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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");

View File

@@ -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",

View File

@@ -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 = (

View File

@@ -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(),

View File

@@ -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:

View File

@@ -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(),

View File

@@ -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:

View File

@@ -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()

View File

@@ -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

View File

@@ -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

View File

@@ -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});

View File

@@ -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"))

View File

@@ -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

View File

@@ -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()

View File

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

View File

@@ -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"""

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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 = [

View File

@@ -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();

View File

@@ -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",

View File

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