Merge branch 'develop' of https://github.com/frappe/erpnext into view-projects-in-customer-portal
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json
index a8afb55..3a3b6e3 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/ae_uae_chart_template_standard.json
@@ -437,12 +437,20 @@
},
"Sales": {
"Sales from Other Regions": {
- "Sales from Other Region": {}
+ "Sales from Other Region": {
+ "account_type": "Income Account"
+ }
},
"Sales of same region": {
- "Management Consultancy Fees 1": {},
- "Sales Account": {},
- "Sales of I/C": {}
+ "Management Consultancy Fees 1": {
+ "account_type": "Income Account"
+ },
+ "Sales Account": {
+ "account_type": "Income Account"
+ },
+ "Sales of I/C": {
+ "account_type": "Income Account"
+ }
}
},
"root_type": "Income"
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
index d1a0def..fb97476 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/id_chart_of_accounts.json
@@ -69,8 +69,7 @@
"Persediaan Barang": {
"Persediaan Barang": {
"account_number": "1141.000",
- "account_type": "Stock",
- "is_group": 1
+ "account_type": "Stock"
},
"Uang Muka Pembelian": {
"Uang Muka Pembelian": {
@@ -670,7 +669,8 @@
},
"Penjualan Barang Dagangan": {
"Penjualan": {
- "account_number": "4110.000"
+ "account_number": "4110.000",
+ "account_type": "Income Account"
},
"Potongan Penjualan": {
"account_number": "4130.000"
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index 8f9f7ce..c8bf664 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -702,7 +702,50 @@
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)
diff --git a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
index fc6dbba..ce9579e 100644
--- a/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
+++ b/erpnext/accounts/doctype/payment_ledger_entry/test_payment_ledger_entry.py
@@ -294,7 +294,7 @@
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 @@
.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 @@
"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
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index 2adc123..7b7ce7a 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -163,6 +163,15 @@
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");
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index 5f6c703..b88791d 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -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",
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 3a9e80a..7ef5278 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -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 @@
}
)
+ 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 @@
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 @@
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 @@
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 @@
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 @@
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 = (
@@ -401,59 +412,6 @@
self.get_unreconciled_entries()
- def make_difference_entry(self, row):
- journal_entry = frappe.new_doc("Journal Entry")
- journal_entry.voucher_type = "Exchange Gain Or Loss"
- journal_entry.company = self.company
- journal_entry.posting_date = nowdate()
- journal_entry.multi_currency = 1
-
- party_account_currency = frappe.get_cached_value(
- "Account", self.receivable_payable_account, "account_currency"
- )
- difference_account_currency = frappe.get_cached_value(
- "Account", row.difference_account, "account_currency"
- )
-
- # Account Currency has balance
- dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
- reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
-
- journal_account = frappe._dict(
- {
- "account": self.receivable_payable_account,
- "party_type": self.party_type,
- "party": self.party,
- "account_currency": party_account_currency,
- "exchange_rate": 0,
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": row.against_voucher_type,
- "reference_name": row.against_voucher,
- dr_or_cr: flt(row.difference_amount),
- dr_or_cr + "_in_account_currency": 0,
- }
- )
-
- journal_entry.append("accounts", journal_account)
-
- journal_account = frappe._dict(
- {
- "account": row.difference_account,
- "account_currency": difference_account_currency,
- "exchange_rate": 1,
- "cost_center": erpnext.get_default_cost_center(self.company),
- reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
- reverse_dr_or_cr: flt(row.difference_amount),
- }
- )
-
- journal_entry.append("accounts", journal_account)
-
- journal_entry.save()
- journal_entry.submit()
-
- return journal_entry
-
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
{
@@ -619,16 +577,6 @@
def reconcile_dr_cr_note(dr_cr_notes, company):
- def get_difference_row(inv):
- if inv.difference_amount != 0 and inv.difference_account:
- difference_row = {
- "account": inv.difference_account,
- inv.dr_or_cr: abs(inv.difference_amount) if inv.difference_amount > 0 else 0,
- reconcile_dr_or_cr: abs(inv.difference_amount) if inv.difference_amount < 0 else 0,
- "cost_center": erpnext.get_default_cost_center(company),
- }
- return difference_row
-
for inv in dr_cr_notes:
voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 6f0b801..ae132eb 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -131,6 +131,7 @@
args: { "pos_profile": frm.pos_profile },
callback: ({ message: profile }) => {
this.update_customer_groups_settings(profile?.customer_groups);
+ this.frm.set_value("company", profile?.company);
},
});
}
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 89a9611..842f159 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -49,6 +49,7 @@
self.validate_pos()
self.validate_payment_amount()
self.validate_loyalty_transaction()
+ self.validate_company_with_pos_company()
if self.coupon_code:
from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code
@@ -281,6 +282,14 @@
if total_amount_in_payments and total_amount_in_payments < invoice_total:
frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total))
+ def validate_company_with_pos_company(self):
+ if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"):
+ frappe.throw(
+ _("Company {} does not match with POS Profile Company {}").format(
+ self.company, frappe.db.get_value("POS Profile", self.pos_profile, "company")
+ )
+ )
+
def validate_loyalty_transaction(self):
if self.redeem_loyalty_points and (
not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center
@@ -359,6 +368,7 @@
profile = {}
if self.pos_profile:
profile = frappe.get_doc("POS Profile", self.pos_profile)
+ self.company = profile.get("company")
if not self.get("payments") and not for_validate:
update_multi_mode_option(self, profile)
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 66438a7..efe9741 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -86,8 +86,7 @@
}
}
- 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(),
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index f334399..9f1224d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -628,9 +628,7 @@
"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 @@
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:
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index a4bcdb4..642e99c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -98,8 +98,7 @@
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(),
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 7581366..e5adeae 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -716,6 +716,7 @@
"fieldtype": "Table",
"hide_days": 1,
"hide_seconds": 1,
+ "label": "Items",
"oldfieldname": "entries",
"oldfieldtype": "Table",
"options": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 0bc5aa2..fba2fa7 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1104,9 +1104,7 @@
"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 @@
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:
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index f9cfe5a..21b39d7 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1500,8 +1500,8 @@
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()
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 954b4e7..de2f9e7 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -271,9 +271,9 @@
net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details
)
else:
- tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
+ tax_amount = net_total * tax_details.rate / 100
else:
- tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
+ tax_amount = net_total * tax_details.rate / 100
# once tds is deducted, not need to add vouchers in the invoice
voucher_wise_amount = {}
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 3803836..d496778 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -539,6 +539,10 @@
"Company", company, ["round_off_account", "round_off_cost_center"]
) or [None, None]
+ # Use expense account as fallback
+ if not round_off_account:
+ round_off_account = frappe.get_cached_value("Company", company, "default_expense_account")
+
meta = frappe.get_meta(voucher_type)
# Give first preference to parent cost center for round off GLE
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 0d67752..8bd7b5a 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -14,7 +14,7 @@
from frappe.contacts.doctype.contact.contact import get_contact_details
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
-from frappe.query_builder.functions import Date, Sum
+from frappe.query_builder.functions import Abs, Date, Sum
from frappe.utils import (
add_days,
add_months,
@@ -922,35 +922,34 @@
def get_partywise_advanced_payment_amount(
- party_type, posting_date=None, future_payment=0, company=None, party=None, account_type=None
+ party_type, posting_date=None, future_payment=0, company=None, party=None
):
- gle = frappe.qb.DocType("GL Entry")
+ ple = frappe.qb.DocType("Payment Ledger Entry")
query = (
- frappe.qb.from_(gle)
- .select(gle.party)
+ frappe.qb.from_(ple)
+ .select(ple.party, Abs(Sum(ple.amount).as_("amount")))
.where(
- (gle.party_type.isin(party_type)) & (gle.against_voucher.isnull()) & (gle.is_cancelled == 0)
+ (ple.party_type.isin(party_type))
+ & (ple.amount < 0)
+ & (ple.against_voucher_no == ple.voucher_no)
+ & (ple.delinked == 0)
)
- .groupby(gle.party)
+ .groupby(ple.party)
)
- if account_type == "Receivable":
- query = query.select(Sum(gle.credit).as_("amount"))
- else:
- query = query.select(Sum(gle.debit).as_("amount"))
if posting_date:
if future_payment:
- query = query.where((gle.posting_date <= posting_date) | (Date(gle.creation) <= posting_date))
+ query = query.where((ple.posting_date <= posting_date) | (Date(ple.creation) <= posting_date))
else:
- query = query.where(gle.posting_date <= posting_date)
+ query = query.where(ple.posting_date <= posting_date)
if company:
- query = query.where(gle.company == company)
+ query = query.where(ple.company == company)
if party:
- query = query.where(gle.party == party)
+ query = query.where(ple.party == party)
- data = query.run(as_dict=True)
+ data = query.run()
if data:
return frappe._dict(data)
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index f78a840..751063a 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -214,8 +214,8 @@
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
@@ -1090,7 +1090,10 @@
.where(
(je.company == self.filters.company)
& (je.posting_date.lte(self.filters.report_date))
- & (je.voucher_type == "Exchange Rate Revaluation")
+ & (
+ (je.voucher_type == "Exchange Rate Revaluation")
+ | (je.voucher_type == "Exchange Gain Or Loss")
+ )
)
.run()
)
diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
index 6f1889b..0c7d931 100644
--- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py
@@ -8,20 +8,17 @@
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.report.accounts_receivable.accounts_receivable import execute
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
-class TestAccountsReceivable(FrappeTestCase):
+class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
def setUp(self):
- frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company 2'")
- frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
- frappe.db.sql("delete from `tabPayment Entry` where company='_Test Company 2'")
- frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'")
- frappe.db.sql("delete from `tabPayment Ledger Entry` where company='_Test Company 2'")
- frappe.db.sql("delete from `tabJournal Entry` where company='_Test Company 2'")
- frappe.db.sql("delete from `tabExchange Rate Revaluation` where company='_Test Company 2'")
-
- self.create_usd_account()
+ self.create_company()
+ self.create_customer()
+ self.create_item()
+ self.create_usd_receivable_account()
+ self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
@@ -49,29 +46,84 @@
debtors_usd.account_type = debtors.account_type
self.debtors_usd = debtors_usd.save().name
+ def create_sales_invoice(self, no_payment_schedule=False, do_not_submit=False):
+ frappe.set_user("Administrator")
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ price_list_rate=100,
+ do_not_save=1,
+ )
+ if not no_payment_schedule:
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
+ )
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
+ )
+ si.append(
+ "payment_schedule",
+ dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
+ )
+ si = si.save()
+ if not do_not_submit:
+ si = si.submit()
+ return si
+
+ def create_payment_entry(self, docname):
+ pe = get_payment_entry("Sales Invoice", docname, bank_account=self.cash, party_amount=40)
+ pe.paid_from = self.debit_to
+ pe.insert()
+ pe.submit()
+
+ def create_credit_note(self, docname):
+ credit_note = create_sales_invoice(
+ company=self.company,
+ customer=self.customer,
+ item=self.item,
+ qty=-1,
+ debit_to=self.debit_to,
+ cost_center=self.cost_center,
+ is_return=1,
+ return_against=docname,
+ )
+
+ return credit_note
+
def test_accounts_receivable(self):
filters = {
- "company": "_Test Company 2",
+ "company": self.company,
"based_on_payment_terms": 1,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
+ "show_remarks": True,
}
# check invoice grand total and invoiced column's value for 3 payment terms
- name = make_sales_invoice().name
+ si = self.create_sales_invoice()
+ name = si.name
+
report = execute(filters)
- expected_data = [[100, 30], [100, 50], [100, 20]]
+ expected_data = [[100, 30, "No Remarks"], [100, 50, "No Remarks"], [100, 20, "No Remarks"]]
for i in range(3):
row = report[1][i - 1]
- self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced])
+ self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced, row.remarks])
# check invoice grand total, invoiced, paid and outstanding column's value after payment
- make_payment(name)
+ self.create_payment_entry(si.name)
report = execute(filters)
expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]]
@@ -84,10 +136,10 @@
)
# check invoice grand total, invoiced, paid and outstanding column's value after credit note
- make_credit_note(name)
+ self.create_credit_note(si.name)
report = execute(filters)
- expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"]
+ expected_data_after_credit_note = [100, 0, 0, 40, -40, self.debit_to]
row = report[1][0]
self.assertEqual(
@@ -108,21 +160,20 @@
"""
so = make_sales_order(
- company="_Test Company 2",
- customer="_Test Customer 2",
- warehouse="Finished Goods - _TC2",
- currency="EUR",
- debit_to="Debtors - _TC2",
- income_account="Sales - _TC2",
- expense_account="Cost of Goods Sold - _TC2",
- cost_center="Main - _TC2",
+ company=self.company,
+ customer=self.customer,
+ warehouse=self.warehouse,
+ debit_to=self.debit_to,
+ income_account=self.income_account,
+ expense_account=self.expense_account,
+ cost_center=self.cost_center,
)
pe = get_payment_entry(so.doctype, so.name)
pe = pe.save().submit()
filters = {
- "company": "_Test Company 2",
+ "company": self.company,
"based_on_payment_terms": 0,
"report_date": today(),
"range1": 30,
@@ -147,34 +198,32 @@
)
@change_settings(
- "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1}
+ "Accounts Settings",
+ {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
)
def test_exchange_revaluation_for_party(self):
"""
- Exchange Revaluation for party on Receivable/Payable shoule be included
+ Exchange Revaluation for party on Receivable/Payable should be included
"""
- company = "_Test Company 2"
- customer = "_Test Customer 2"
-
# Using Exchange Gain/Loss account for unrealized as well.
- company_doc = frappe.get_doc("Company", company)
+ company_doc = frappe.get_doc("Company", self.company)
company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
company_doc.save()
- si = make_sales_invoice(no_payment_schedule=True, do_not_submit=True)
+ si = self.create_sales_invoice(no_payment_schedule=True, do_not_submit=True)
si.currency = "USD"
- si.conversion_rate = 0.90
+ si.conversion_rate = 80
si.debit_to = self.debtors_usd
si = si.save().submit()
# Exchange Revaluation
err = frappe.new_doc("Exchange Rate Revaluation")
- err.company = company
+ err.company = self.company
err.posting_date = today()
accounts = err.get_accounts_data()
err.extend("accounts", accounts)
- err.accounts[0].new_exchange_rate = 0.95
+ err.accounts[0].new_exchange_rate = 85
row = err.accounts[0]
row.new_balance_in_base_currency = flt(
row.new_exchange_rate * flt(row.balance_in_account_currency)
@@ -189,7 +238,7 @@
je = je.submit()
filters = {
- "company": company,
+ "company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
@@ -198,7 +247,7 @@
}
report = execute(filters)
- expected_data_for_err = [0, -5, 0, 5]
+ expected_data_for_err = [0, -500, 0, 500]
row = [x for x in report[1] if x.voucher_type == je.doctype and x.voucher_no == je.name][0]
self.assertEqual(
expected_data_for_err,
@@ -214,46 +263,43 @@
"""
Payment against credit/debit note should be considered against the parent invoice
"""
- company = "_Test Company 2"
- customer = "_Test Customer 2"
- si1 = make_sales_invoice()
+ si1 = self.create_sales_invoice()
- pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
- pe.paid_from = "Debtors - _TC2"
+ pe = get_payment_entry(si1.doctype, si1.name, bank_account=self.cash)
+ pe.paid_from = self.debit_to
pe.insert()
pe.submit()
- cr_note = make_credit_note(si1.name)
+ cr_note = self.create_credit_note(si1.name)
- si2 = make_sales_invoice()
+ si2 = self.create_sales_invoice()
# manually link cr_note with si2 using journal entry
je = frappe.new_doc("Journal Entry")
- je.company = company
+ je.company = self.company
je.voucher_type = "Credit Note"
je.posting_date = today()
- debit_account = "Debtors - _TC2"
debit_entry = {
- "account": debit_account,
+ "account": self.debit_to,
"party_type": "Customer",
- "party": customer,
+ "party": self.customer,
"debit": 100,
"debit_in_account_currency": 100,
"reference_type": cr_note.doctype,
"reference_name": cr_note.name,
- "cost_center": "Main - _TC2",
+ "cost_center": self.cost_center,
}
credit_entry = {
- "account": debit_account,
+ "account": self.debit_to,
"party_type": "Customer",
- "party": customer,
+ "party": self.customer,
"credit": 100,
"credit_in_account_currency": 100,
"reference_type": si2.doctype,
"reference_name": si2.name,
- "cost_center": "Main - _TC2",
+ "cost_center": self.cost_center,
}
je.append("accounts", debit_entry)
@@ -261,7 +307,7 @@
je = je.save().submit()
filters = {
- "company": company,
+ "company": self.company,
"report_date": today(),
"range1": 30,
"range2": 60,
@@ -271,64 +317,254 @@
report = execute(filters)
self.assertEqual(report[1], [])
+ def test_group_by_party(self):
+ si1 = self.create_sales_invoice(do_not_submit=True)
+ si1.posting_date = add_days(today(), -1)
+ si1.save().submit()
+ si2 = self.create_sales_invoice(do_not_submit=True)
+ si2.items[0].rate = 85
+ si2.save().submit()
-def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
- frappe.set_user("Administrator")
+ filters = {
+ "company": self.company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "group_by_party": True,
+ }
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 5)
- si = create_sales_invoice(
- company="_Test Company 2",
- customer="_Test Customer 2",
- currency="EUR",
- warehouse="Finished Goods - _TC2",
- debit_to="Debtors - _TC2",
- income_account="Sales - _TC2",
- expense_account="Cost of Goods Sold - _TC2",
- cost_center="Main - _TC2",
- do_not_save=1,
- )
+ # assert voucher rows
+ expected_voucher_rows = [
+ [100.0, 100.0, 100.0, 100.0],
+ [85.0, 85.0, 85.0, 85.0],
+ ]
+ voucher_rows = []
+ for x in report[0:2]:
+ voucher_rows.append(
+ [x.invoiced, x.outstanding, x.invoiced_in_account_currency, x.outstanding_in_account_currency]
+ )
+ self.assertEqual(expected_voucher_rows, voucher_rows)
- if not no_payment_schedule:
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30),
+ # assert total rows
+ expected_total_rows = [
+ [self.customer, 185.0, 185.0], # party total
+ {}, # empty row for padding
+ ["Total", 185.0, 185.0], # grand total
+ ]
+ party_total_row = report[2]
+ self.assertEqual(
+ expected_total_rows[0],
+ [
+ party_total_row.get("party"),
+ party_total_row.get("invoiced"),
+ party_total_row.get("outstanding"),
+ ],
)
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50),
- )
- si.append(
- "payment_schedule",
- dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20),
+ empty_row = report[3]
+ self.assertEqual(expected_total_rows[1], empty_row)
+ grand_total_row = report[4]
+ self.assertEqual(
+ expected_total_rows[2],
+ [
+ grand_total_row.get("party"),
+ grand_total_row.get("invoiced"),
+ grand_total_row.get("outstanding"),
+ ],
)
- si = si.save()
+ def test_future_payments(self):
+ si = self.create_sales_invoice()
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.posting_date = add_days(today(), 1)
+ pe.paid_amount = 90.0
+ pe.references[0].allocated_amount = 90.0
+ pe.save().submit()
+ filters = {
+ "company": self.company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "show_future_payments": True,
+ }
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
- if not do_not_submit:
- si = si.submit()
+ expected_data = [100.0, 100.0, 10.0, 90.0]
- return si
+ row = report[0]
+ self.assertEqual(
+ expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
+ )
+ pe.cancel()
+ # full payment in future date
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.posting_date = add_days(today(), 1)
+ pe.save().submit()
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+ expected_data = [100.0, 100.0, 0.0, 100.0]
+ row = report[0]
+ self.assertEqual(
+ expected_data, [row.invoiced, row.outstanding, row.remaining_balance, row.future_amount]
+ )
-def make_payment(docname):
- pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40)
- pe.paid_from = "Debtors - _TC2"
- pe.insert()
- pe.submit()
+ pe.cancel()
+ # over payment in future date
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.posting_date = add_days(today(), 1)
+ pe.paid_amount = 110
+ pe.save().submit()
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 2)
+ expected_data = [[100.0, 0.0, 100.0, 0.0, 100.0], [0.0, 10.0, -10.0, -10.0, 0.0]]
+ for idx, row in enumerate(report):
+ self.assertEqual(
+ expected_data[idx],
+ [row.invoiced, row.paid, row.outstanding, row.remaining_balance, row.future_amount],
+ )
+ def test_sales_person(self):
+ sales_person = (
+ frappe.get_doc({"doctype": "Sales Person", "sales_person_name": "John Clark", "enabled": True})
+ .insert()
+ .submit()
+ )
+ si = self.create_sales_invoice(do_not_submit=True)
+ si.append("sales_team", {"sales_person": sales_person.name, "allocated_percentage": 100})
+ si.save().submit()
-def make_credit_note(docname):
- credit_note = create_sales_invoice(
- company="_Test Company 2",
- customer="_Test Customer 2",
- currency="EUR",
- qty=-1,
- warehouse="Finished Goods - _TC2",
- debit_to="Debtors - _TC2",
- income_account="Sales - _TC2",
- expense_account="Cost of Goods Sold - _TC2",
- cost_center="Main - _TC2",
- is_return=1,
- return_against=docname,
- )
+ filters = {
+ "company": self.company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "sales_person": sales_person.name,
+ "show_sales_person": True,
+ }
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
- return credit_note
+ expected_data = [100.0, 100.0, sales_person.name]
+
+ row = report[0]
+ self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.sales_person])
+
+ def test_cost_center_filter(self):
+ si = self.create_sales_invoice()
+ filters = {
+ "company": self.company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "cost_center": self.cost_center,
+ }
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+ expected_data = [100.0, 100.0, self.cost_center]
+ row = report[0]
+ self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.cost_center])
+
+ def test_customer_group_filter(self):
+ si = self.create_sales_invoice()
+ cus_group = frappe.db.get_value("Customer", self.customer, "customer_group")
+ filters = {
+ "company": self.company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "customer_group": cus_group,
+ }
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+ expected_data = [100.0, 100.0, cus_group]
+ row = report[0]
+ self.assertEqual(expected_data, [row.invoiced, row.outstanding, row.customer_group])
+
+ filters.update({"customer_group": "Individual"})
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 0)
+
+ def test_party_account_filter(self):
+ si1 = self.create_sales_invoice()
+ self.customer2 = (
+ frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_name": "Jane Doe",
+ "type": "Individual",
+ "default_currency": "USD",
+ }
+ )
+ .insert()
+ .submit()
+ )
+
+ si2 = self.create_sales_invoice(do_not_submit=True)
+ si2.posting_date = add_days(today(), -1)
+ si2.customer = self.customer2
+ si2.currency = "USD"
+ si2.conversion_rate = 80
+ si2.debit_to = self.debtors_usd
+ si2.save().submit()
+
+ # Filter on company currency receivable account
+ filters = {
+ "company": self.company,
+ "report_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ "party_account": self.debit_to,
+ }
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+ expected_data = [100.0, 100.0, self.debit_to, si1.currency]
+ row = report[0]
+ self.assertEqual(
+ expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
+ )
+
+ # Filter on USD receivable account
+ filters.update({"party_account": self.debtors_usd})
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 1)
+ expected_data = [8000.0, 8000.0, self.debtors_usd, si2.currency]
+ row = report[0]
+ self.assertEqual(
+ expected_data, [row.invoiced, row.outstanding, row.party_account, row.account_currency]
+ )
+
+ # without filter on party account
+ filters.pop("party_account")
+ report = execute(filters)[1]
+ self.assertEqual(len(report), 2)
+ expected_data = [
+ [8000.0, 8000.0, 100.0, 100.0, self.debtors_usd, si2.currency],
+ [100.0, 100.0, 100.0, 100.0, self.debit_to, si1.currency],
+ ]
+ for idx, row in enumerate(report):
+ self.assertEqual(
+ expected_data[idx],
+ [
+ row.invoiced,
+ row.outstanding,
+ row.invoiced_in_account_currency,
+ row.outstanding_in_account_currency,
+ row.party_account,
+ row.account_currency,
+ ],
+ )
diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
index da4c9da..cffc878 100644
--- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
+++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py
@@ -50,13 +50,12 @@
self.filters.show_future_payments,
self.filters.company,
party=party,
- account_type=self.account_type,
)
or {}
)
if self.filters.show_gl_balance:
- gl_balance_map = get_gl_balance(self.filters.report_date)
+ gl_balance_map = get_gl_balance(self.filters.report_date, self.filters.company)
for party, party_dict in self.party_total.items():
if party_dict.outstanding == 0:
@@ -233,12 +232,12 @@
self.add_column(label="Total Amount Due", fieldname="total_due")
-def get_gl_balance(report_date):
+def get_gl_balance(report_date, company):
return frappe._dict(
frappe.db.get_all(
"GL Entry",
fields=["party", "sum(debit - credit)"],
- filters={"posting_date": ("<=", report_date), "is_cancelled": 0},
+ filters={"posting_date": ("<=", report_date), "is_cancelled": 0, "company": company},
group_by="party",
as_list=1,
)
diff --git a/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
new file mode 100644
index 0000000..3ee35a1
--- /dev/null
+++ b/erpnext/accounts/report/accounts_receivable_summary/test_accounts_receivable_summary.py
@@ -0,0 +1,203 @@
+import unittest
+
+import frappe
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import today
+
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.report.accounts_receivable_summary.accounts_receivable_summary import execute
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+
+
+class TestAccountsReceivable(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.maxDiff = None
+ self.create_company()
+ self.create_customer()
+ self.create_item()
+ self.clear_old_entries()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def test_01_receivable_summary_output(self):
+ """
+ Test for Invoices, Paid, Advance and Outstanding
+ """
+ filters = {
+ "company": self.company,
+ "customer": self.customer,
+ "posting_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=200,
+ price_list_rate=200,
+ )
+
+ customer_group, customer_territory = frappe.db.get_all(
+ "Customer",
+ filters={"name": self.customer},
+ fields=["customer_group", "territory"],
+ as_list=True,
+ )[0]
+
+ report = execute(filters)
+ rpt_output = report[1]
+ expected_data = {
+ "party_type": "Customer",
+ "advance": 0,
+ "party": self.customer,
+ "invoiced": 200.0,
+ "paid": 0.0,
+ "credit_note": 0.0,
+ "outstanding": 200.0,
+ "range1": 200.0,
+ "range2": 0.0,
+ "range3": 0.0,
+ "range4": 0.0,
+ "range5": 0.0,
+ "total_due": 200.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "currency": si.currency,
+ "territory": customer_territory,
+ "customer_group": customer_group,
+ }
+
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # simulate advance payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 50
+ pe.references[0].allocated_amount = 0 # this essitially removes the reference
+ pe.save().submit()
+
+ # update expected data with advance
+ expected_data.update(
+ {
+ "advance": 50.0,
+ "outstanding": 150.0,
+ "range1": 150.0,
+ "total_due": 150.0,
+ }
+ )
+
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # make partial payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 125
+ pe.references[0].allocated_amount = 125
+ pe.save().submit()
+
+ # update expected data after advance and partial payment
+ expected_data.update(
+ {"advance": 50.0, "paid": 125.0, "outstanding": 25.0, "range1": 25.0, "total_due": 25.0}
+ )
+
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ @change_settings("Selling Settings", {"cust_master_name": "Naming Series"})
+ def test_02_various_filters_and_output(self):
+ filters = {
+ "company": self.company,
+ "customer": self.customer,
+ "posting_date": today(),
+ "range1": 30,
+ "range2": 60,
+ "range3": 90,
+ "range4": 120,
+ }
+
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debit_to,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=200,
+ price_list_rate=200,
+ )
+ # make partial payment
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 150
+ pe.references[0].allocated_amount = 150
+ pe.save().submit()
+
+ customer_group, customer_territory = frappe.db.get_all(
+ "Customer",
+ filters={"name": self.customer},
+ fields=["customer_group", "territory"],
+ as_list=True,
+ )[0]
+
+ report = execute(filters)
+ rpt_output = report[1]
+ expected_data = {
+ "party_type": "Customer",
+ "advance": 0,
+ "party": self.customer,
+ "party_name": self.customer,
+ "invoiced": 200.0,
+ "paid": 150.0,
+ "credit_note": 0.0,
+ "outstanding": 50.0,
+ "range1": 50.0,
+ "range2": 0.0,
+ "range3": 0.0,
+ "range4": 0.0,
+ "range5": 0.0,
+ "total_due": 50.0,
+ "future_amount": 0.0,
+ "sales_person": [],
+ "currency": si.currency,
+ "territory": customer_territory,
+ "customer_group": customer_group,
+ }
+
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # with gl balance filter
+ filters.update({"show_gl_balance": True})
+ expected_data.update({"gl_balance": 50.0, "diff": 0.0})
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # with gl balance and future payments filter
+ filters.update({"show_future_payments": True})
+ expected_data.update({"remaining_balance": 50.0})
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 1)
+ self.assertDictEqual(rpt_output[0], expected_data)
+
+ # invoice fully paid
+ pe = get_payment_entry(si.doctype, si.name).save().submit()
+ report = execute(filters)
+ rpt_output = report[1]
+ self.assertEqual(len(rpt_output), 0)
diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
index d67eee3..bdc8d85 100644
--- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
+++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py
@@ -58,6 +58,9 @@
def get_asset_categories(filters):
+ condition = ""
+ if filters.get("asset_category"):
+ condition += " and asset_category = %(asset_category)s"
return frappe.db.sql(
"""
SELECT asset_category,
@@ -98,15 +101,25 @@
0
end), 0) as cost_of_scrapped_asset
from `tabAsset`
- where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s
+ where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s {}
group by asset_category
- """,
- {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
+ """.format(
+ condition
+ ),
+ {
+ "to_date": filters.to_date,
+ "from_date": filters.from_date,
+ "company": filters.company,
+ "asset_category": filters.get("asset_category"),
+ },
as_dict=1,
)
def get_assets(filters):
+ condition = ""
+ if filters.get("asset_category"):
+ condition = " and a.asset_category = '{}'".format(filters.get("asset_category"))
return frappe.db.sql(
"""
SELECT results.asset_category,
@@ -138,7 +151,7 @@
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
- where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account) {0}
group by a.asset_category
union
SELECT a.asset_category,
@@ -154,10 +167,12 @@
end), 0) as depreciation_eliminated_during_the_period,
0 as depreciation_amount_during_the_period
from `tabAsset` a
- where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s
+ where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s {0}
group by a.asset_category) as results
group by results.asset_category
- """,
+ """.format(
+ condition
+ ),
{"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company},
as_dict=1,
)
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index 080e45a..0051ba6 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -744,13 +744,18 @@
if from_date:
additional_conditions.append(gle.posting_date >= from_date)
- finance_book = filters.get("finance_book")
- company_fb = frappe.get_cached_value("Company", d.name, "default_finance_book")
+ finance_books = []
+ finance_books.append("")
+ if filter_fb := filters.get("finance_book"):
+ finance_books.append(filter_fb)
if filters.get("include_default_book_entries"):
- additional_conditions.append((gle.finance_book.isin([finance_book, company_fb, "", None])))
+ if company_fb := frappe.get_cached_value("Company", d.name, "default_finance_book"):
+ finance_books.append(company_fb)
+
+ additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
else:
- additional_conditions.append((gle.finance_book.isin([finance_book, "", None])))
+ additional_conditions.append((gle.finance_book.isin(finance_books)) | gle.finance_book.isnull())
return additional_conditions
diff --git a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
index 7d16661..7191720 100644
--- a/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
+++ b/erpnext/accounts/report/tax_withholding_details/tax_withholding_details.py
@@ -257,7 +257,7 @@
}
party = frappe.get_all(filters.get("party_type"), pluck="name")
- query_filters.update({"against": ("in", party)})
+ or_filters.update({"against": ("in", party), "voucher_type": "Journal Entry"})
if filters.get("party"):
del query_filters["account"]
@@ -294,7 +294,7 @@
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
- get_doc_info(journal_entries, "Journal Entry", tax_category_map)
+ get_doc_info(journal_entries, "Journal Entry", tax_category_map, net_total_map)
return (
tds_documents,
@@ -309,7 +309,11 @@
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
- {"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
+ {
+ "parent": ("in", journal_entries),
+ "party_type": ("in", ("Supplier", "Customer")),
+ "party": ("is", "set"),
+ },
["parent", "party"],
):
if d.parent not in journal_entry_party_map:
@@ -320,41 +324,29 @@
def get_doc_info(vouchers, doctype, tax_category_map, net_total_map=None):
- if doctype == "Purchase Invoice":
- fields = [
- "name",
- "tax_withholding_category",
- "base_tax_withholding_net_total",
- "grand_total",
- "base_total",
- ]
- elif doctype == "Sales Invoice":
- fields = ["name", "base_net_total", "grand_total", "base_total"]
- elif doctype == "Payment Entry":
- fields = [
- "name",
- "tax_withholding_category",
- "paid_amount",
- "paid_amount_after_tax",
- "base_paid_amount",
- ]
- else:
- fields = ["name", "tax_withholding_category"]
+ common_fields = ["name", "tax_withholding_category"]
+ fields_dict = {
+ "Purchase Invoice": ["base_tax_withholding_net_total", "grand_total", "base_total"],
+ "Sales Invoice": ["base_net_total", "grand_total", "base_total"],
+ "Payment Entry": ["paid_amount", "paid_amount_after_tax", "base_paid_amount"],
+ "Journal Entry": ["total_amount"],
+ }
- entries = frappe.get_all(doctype, filters={"name": ("in", vouchers)}, fields=fields)
+ entries = frappe.get_all(
+ doctype, filters={"name": ("in", vouchers)}, fields=common_fields + fields_dict[doctype]
+ )
for entry in entries:
tax_category_map.update({entry.name: entry.tax_withholding_category})
if doctype == "Purchase Invoice":
- net_total_map.update(
- {entry.name: [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]}
- )
+ value = [entry.base_tax_withholding_net_total, entry.grand_total, entry.base_total]
elif doctype == "Sales Invoice":
- net_total_map.update({entry.name: [entry.base_net_total, entry.grand_total, entry.base_total]})
+ value = [entry.base_net_total, entry.grand_total, entry.base_total]
elif doctype == "Payment Entry":
- net_total_map.update(
- {entry.name: [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]}
- )
+ value = [entry.paid_amount, entry.paid_amount_after_tax, entry.base_paid_amount]
+ else:
+ value = [entry.total_amount] * 3
+ net_total_map.update({entry.name: value})
def get_tax_rate_map(filters):
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index 70bbf7e..bf01362 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -1,4 +1,5 @@
import frappe
+from frappe import qb
from erpnext.stock.doctype.item.test_item import create_item
@@ -59,7 +60,6 @@
self.income_account = "Sales - " + abbr
self.expense_account = "Cost of Goods Sold - " + abbr
self.debit_to = "Debtors - " + abbr
- self.debit_usd = "Debtors USD - " + abbr
self.cash = "Cash - " + abbr
self.creditors = "Creditors - " + abbr
self.retained_earnings = "Retained Earnings - " + abbr
@@ -103,3 +103,39 @@
)
new_acc.save()
setattr(self, acc.attribute_name, new_acc.name)
+
+ def create_usd_receivable_account(self):
+ account_name = "Debtors USD"
+ if not frappe.db.get_value(
+ "Account", filters={"account_name": account_name, "company": self.company}
+ ):
+ acc = frappe.new_doc("Account")
+ acc.account_name = account_name
+ acc.parent_account = "Accounts Receivable - " + self.company_abbr
+ acc.company = self.company
+ acc.account_currency = "USD"
+ acc.account_type = "Receivable"
+ acc.insert()
+ else:
+ name = frappe.db.get_value(
+ "Account",
+ filters={"account_name": account_name, "company": self.company},
+ fieldname="name",
+ pluck=True,
+ )
+ acc = frappe.get_doc("Account", name)
+ self.debtors_usd = acc.name
+
+ def clear_old_entries(self):
+ doctype_list = [
+ "GL Entry",
+ "Payment Ledger Entry",
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Payment Entry",
+ "Journal Entry",
+ "Sales Order",
+ "Exchange Rate Revaluation",
+ ]
+ for doctype in doctype_list:
+ qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index bccf6f1..1aefeaa 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -908,7 +908,9 @@
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 @@
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 @@
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 @@
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 @@
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 @@
)
)
+ 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 @@
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 @@
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
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index 0a2f61d..962292b 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -228,15 +228,19 @@
{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});
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 2060c6c..ddb09c1 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -56,7 +56,6 @@
def on_submit(self):
self.validate_in_use_date()
- self.set_status()
self.make_asset_movement()
if not self.booked_fixed_asset and self.validate_make_gl_entry():
self.make_gl_entries()
@@ -72,6 +71,7 @@
"Asset Depreciation Schedules created:<br>{0}<br><br>Please check, edit if needed, and submit the Asset."
).format(asset_depr_schedules_links)
)
+ self.set_status()
add_asset_activity(self.name, _("Asset submitted"))
def on_cancel(self):
@@ -96,11 +96,14 @@
"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"))
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index cd66f1d..90eae2d 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -19,6 +19,7 @@
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.assets.doctype.asset.asset import (
+ get_asset_value_after_depreciation,
make_sales_invoice,
split_asset,
update_maintenance_status,
diff --git a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
index 324b739..0bf2fbb 100644
--- a/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
+++ b/erpnext/assets/doctype/asset_capitalization/asset_capitalization.py
@@ -509,6 +509,7 @@
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
diff --git a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
index 39ebd4e..83350aa 100644
--- a/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
+++ b/erpnext/assets/doctype/asset_depreciation_schedule/asset_depreciation_schedule.py
@@ -107,7 +107,7 @@
have_asset_details_been_modified, not_manual_depr_or_have_manual_depr_details_been_modified
):
self.make_depr_schedule(asset_doc, row, date_of_disposal, update_asset_finance_book_row)
- self.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+ self.set_accumulated_depreciation(asset_doc, row, date_of_disposal, date_of_return)
def have_asset_details_been_modified(self, asset_doc):
return (
@@ -157,7 +157,12 @@
self.status = "Draft"
def make_depr_schedule(
- self, asset_doc, row, date_of_disposal, update_asset_finance_book_row=True
+ self,
+ asset_doc,
+ row,
+ date_of_disposal,
+ update_asset_finance_book_row=True,
+ value_after_depreciation=None,
):
if not self.get("depreciation_schedule"):
self.depreciation_schedule = []
@@ -167,7 +172,9 @@
start = self.clear_depr_schedule()
- self._make_depr_schedule(asset_doc, row, start, date_of_disposal, update_asset_finance_book_row)
+ self._make_depr_schedule(
+ asset_doc, row, start, date_of_disposal, update_asset_finance_book_row, value_after_depreciation
+ )
def clear_depr_schedule(self):
start = 0
@@ -187,23 +194,30 @@
return start
def _make_depr_schedule(
- self, asset_doc, row, start, date_of_disposal, update_asset_finance_book_row
+ self,
+ asset_doc,
+ row,
+ start,
+ date_of_disposal,
+ update_asset_finance_book_row,
+ value_after_depreciation,
):
asset_doc.validate_asset_finance_books(row)
- value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
+ if not value_after_depreciation:
+ value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row:
row.db_update()
- number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
+ final_number_of_depreciations = cint(row.total_number_of_depreciations) - cint(
self.number_of_depreciations_booked
)
has_pro_rata = _check_is_pro_rata(asset_doc, row)
if has_pro_rata:
- number_of_pending_depreciations += 1
+ final_number_of_depreciations += 1
has_wdv_or_dd_non_yearly_pro_rata = False
if (
@@ -219,7 +233,9 @@
depreciation_amount = 0
- for n in range(start, number_of_pending_depreciations):
+ number_of_pending_depreciations = final_number_of_depreciations - start
+
+ for n in range(start, final_number_of_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row:
continue
@@ -236,10 +252,11 @@
n,
prev_depreciation_amount,
has_wdv_or_dd_non_yearly_pro_rata,
+ number_of_pending_depreciations,
)
if not has_pro_rata or (
- n < (cint(number_of_pending_depreciations) - 1) or number_of_pending_depreciations == 2
+ n < (cint(final_number_of_depreciations) - 1) or final_number_of_depreciations == 2
):
schedule_date = add_months(
row.depreciation_start_date, n * cint(row.frequency_of_depreciation)
@@ -310,7 +327,7 @@
)
# For last row
- elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
+ elif has_pro_rata and n == cint(final_number_of_depreciations) - 1:
if not asset_doc.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
asset_doc.to_date = add_months(
@@ -343,7 +360,7 @@
# Adjust depreciation amount in the last period based on the expected value after useful life
if row.expected_value_after_useful_life and (
(
- n == cint(number_of_pending_depreciations) - 1
+ n == cint(final_number_of_depreciations) - 1
and value_after_depreciation != row.expected_value_after_useful_life
)
or value_after_depreciation < row.expected_value_after_useful_life
@@ -392,6 +409,7 @@
def set_accumulated_depreciation(
self,
+ asset_doc,
row,
date_of_disposal=None,
date_of_return=None,
@@ -403,13 +421,21 @@
if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual"
]
- accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+ accumulated_depreciation = None
value_after_depreciation = flt(row.value_after_depreciation)
for i, d in enumerate(self.get("depreciation_schedule")):
if ignore_booked_entry and d.journal_entry:
continue
+ if not accumulated_depreciation:
+ if i > 0 and asset_doc.flags.decrease_in_asset_value_due_to_value_adjustment:
+ accumulated_depreciation = self.get("depreciation_schedule")[
+ i - 1
+ ].accumulated_depreciation_amount
+ else:
+ accumulated_depreciation = flt(self.opening_accumulated_depreciation)
+
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
@@ -507,9 +533,12 @@
schedule_idx=0,
prev_depreciation_amount=0,
has_wdv_or_dd_non_yearly_pro_rata=False,
+ number_of_pending_depreciations=0,
):
if fb_row.depreciation_method in ("Straight Line", "Manual"):
- return get_straight_line_or_manual_depr_amount(asset, fb_row, schedule_idx)
+ return get_straight_line_or_manual_depr_amount(
+ asset, fb_row, schedule_idx, number_of_pending_depreciations
+ )
else:
rate_of_depreciation = get_updated_rate_of_depreciation_for_wdv_and_dd(
asset, depreciable_value, fb_row
@@ -529,7 +558,9 @@
return fb_row.rate_of_depreciation
-def get_straight_line_or_manual_depr_amount(asset, row, schedule_idx):
+def get_straight_line_or_manual_depr_amount(
+ asset, row, schedule_idx, number_of_pending_depreciations
+):
# if the Depreciation Schedule is being modified after Asset Repair due to increase in asset life and value
if asset.flags.increase_in_asset_life:
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / (
@@ -540,6 +571,36 @@
return (flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)) / flt(
row.total_number_of_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:
+ if row.daily_depreciation:
+ daily_depr_amount = (
+ flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
+ ) / date_diff(
+ add_months(
+ row.depreciation_start_date,
+ flt(row.total_number_of_depreciations - asset.number_of_depreciations_booked)
+ * row.frequency_of_depreciation,
+ ),
+ add_months(
+ row.depreciation_start_date,
+ flt(
+ row.total_number_of_depreciations
+ - asset.number_of_depreciations_booked
+ - number_of_pending_depreciations
+ )
+ * row.frequency_of_depreciation,
+ ),
+ )
+ to_date = add_months(row.depreciation_start_date, schedule_idx * row.frequency_of_depreciation)
+ from_date = add_months(
+ row.depreciation_start_date, (schedule_idx - 1) * row.frequency_of_depreciation
+ )
+ return daily_depr_amount * date_diff(to_date, from_date)
+ else:
+ return (
+ flt(row.value_after_depreciation) - flt(row.expected_value_after_useful_life)
+ ) / number_of_pending_depreciations
# if the Depreciation Schedule is being prepared for the first time
else:
if row.daily_depreciation:
@@ -669,7 +730,12 @@
def make_new_active_asset_depr_schedules_and_cancel_current_ones(
- asset_doc, notes, date_of_disposal=None, date_of_return=None
+ asset_doc,
+ notes,
+ date_of_disposal=None,
+ date_of_return=None,
+ value_after_depreciation=None,
+ ignore_booked_entry=False,
):
for row in asset_doc.get("finance_books"):
current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
@@ -695,8 +761,12 @@
row.rate_of_depreciation = new_rate_of_depreciation
new_asset_depr_schedule_doc.rate_of_depreciation = new_rate_of_depreciation
- new_asset_depr_schedule_doc.make_depr_schedule(asset_doc, row, date_of_disposal)
- new_asset_depr_schedule_doc.set_accumulated_depreciation(row, date_of_disposal, date_of_return)
+ new_asset_depr_schedule_doc.make_depr_schedule(
+ asset_doc, row, date_of_disposal, value_after_depreciation=value_after_depreciation
+ )
+ new_asset_depr_schedule_doc.set_accumulated_depreciation(
+ asset_doc, row, date_of_disposal, date_of_return, ignore_booked_entry
+ )
new_asset_depr_schedule_doc.notes = notes
@@ -709,9 +779,20 @@
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,
@@ -719,7 +800,7 @@
update_asset_finance_book_row,
)
- return asset_depr_schedule_doc
+ return temp_asset_depr_schedule_doc
@frappe.whitelist()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 823b6e9..9be7243 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -5,7 +5,7 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from frappe.utils import date_diff, flt, formatdate, get_link_to_form, getdate
+from frappe.utils import flt, formatdate, get_link_to_form, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
@@ -14,8 +14,7 @@
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
- get_asset_depr_schedule_doc,
- get_depreciation_amount,
+ make_new_active_asset_depr_schedules_and_cancel_current_ones,
)
@@ -27,7 +26,7 @@
def on_submit(self):
self.make_depreciation_entry()
- self.reschedule_depreciations(self.new_asset_value)
+ self.update_asset(self.new_asset_value)
add_asset_activity(
self.asset,
_("Asset's value adjusted after submission of Asset Value Adjustment {0}").format(
@@ -36,7 +35,7 @@
)
def on_cancel(self):
- self.reschedule_depreciations(self.current_asset_value)
+ self.update_asset(self.current_asset_value)
add_asset_activity(
self.asset,
_("Asset's value adjusted after cancellation of Asset Value Adjustment {0}").format(
@@ -124,73 +123,33 @@
self.db_set("journal_entry", je.name)
- def reschedule_depreciations(self, asset_value):
+ def update_asset(self, asset_value):
asset = frappe.get_doc("Asset", self.asset)
- country = frappe.get_value("Company", self.company, "country")
- for d in asset.finance_books:
- d.value_after_depreciation = asset_value
+ if not asset.calculate_depreciation:
+ asset.value_after_depreciation = asset_value
+ asset.save()
+ return
- current_asset_depr_schedule_doc = get_asset_depr_schedule_doc(
- asset.name, "Active", d.finance_book
+ asset.flags.decrease_in_asset_value_due_to_value_adjustment = True
+
+ if self.docstatus == 1:
+ notes = _(
+ "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
+ ).format(
+ get_link_to_form("Asset", asset.name),
+ get_link_to_form(self.get("doctype"), self.get("name")),
+ )
+ elif self.docstatus == 2:
+ notes = _(
+ "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
+ ).format(
+ get_link_to_form("Asset", asset.name),
+ get_link_to_form(self.get("doctype"), self.get("name")),
)
- new_asset_depr_schedule_doc = frappe.copy_doc(current_asset_depr_schedule_doc)
- new_asset_depr_schedule_doc.status = "Draft"
- new_asset_depr_schedule_doc.docstatus = 0
-
- current_asset_depr_schedule_doc.flags.should_not_cancel_depreciation_entries = True
- current_asset_depr_schedule_doc.cancel()
-
- if self.docstatus == 1:
- notes = _(
- "This schedule was created when Asset {0} was adjusted through Asset Value Adjustment {1}."
- ).format(
- get_link_to_form(asset.doctype, asset.name),
- get_link_to_form(self.get("doctype"), self.get("name")),
- )
- elif self.docstatus == 2:
- notes = _(
- "This schedule was created when Asset {0}'s Asset Value Adjustment {1} was cancelled."
- ).format(
- get_link_to_form(asset.doctype, asset.name),
- get_link_to_form(self.get("doctype"), self.get("name")),
- )
- new_asset_depr_schedule_doc.notes = notes
-
- new_asset_depr_schedule_doc.insert()
-
- depr_schedule = new_asset_depr_schedule_doc.get("depreciation_schedule")
-
- if d.depreciation_method in ("Straight Line", "Manual"):
- end_date = max(s.schedule_date for s in depr_schedule)
- total_days = date_diff(end_date, self.date)
- rate_per_day = flt(d.value_after_depreciation - d.expected_value_after_useful_life) / flt(
- total_days
- )
- from_date = self.date
- else:
- no_of_depreciations = len([s.name for s in depr_schedule if not s.journal_entry])
-
- value_after_depreciation = d.value_after_depreciation
- for data in depr_schedule:
- if not data.journal_entry:
- if d.depreciation_method in ("Straight Line", "Manual"):
- days = date_diff(data.schedule_date, from_date)
- depreciation_amount = days * rate_per_day
- from_date = data.schedule_date
- else:
- depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
-
- if depreciation_amount:
- value_after_depreciation -= flt(depreciation_amount)
- data.depreciation_amount = depreciation_amount
-
- d.db_update()
-
- new_asset_depr_schedule_doc.set_accumulated_depreciation(d, ignore_booked_entry=True)
- for asset_data in depr_schedule:
- if not asset_data.journal_entry:
- asset_data.db_update()
-
- new_asset_depr_schedule_doc.submit()
+ make_new_active_asset_depr_schedules_and_cancel_current_ones(
+ asset, notes, value_after_depreciation=asset_value, ignore_booked_entry=True
+ )
+ asset.flags.ignore_validate_update_after_submit = True
+ asset.save()
diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
index 0b3dcba..5d49759 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py
@@ -4,9 +4,10 @@
import unittest
import frappe
-from frappe.utils import add_days, get_last_day, nowdate
+from frappe.utils import add_days, cstr, get_last_day, getdate, nowdate
from erpnext.assets.doctype.asset.asset import get_asset_value_after_depreciation
+from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries
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,
@@ -49,27 +50,23 @@
def test_asset_depreciation_value_adjustment(self):
pr = make_purchase_receipt(
- item_code="Macbook Pro", qty=1, rate=100000.0, location="Test Location"
+ 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"
- month_end_date = get_last_day(nowdate())
- purchase_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15)
-
- asset_doc.available_for_use_date = purchase_date
- asset_doc.purchase_date = purchase_date
- asset_doc.calculate_depreciation = 1
asset_doc.append(
"finance_books",
{
"expected_value_after_useful_life": 200,
"depreciation_method": "Straight Line",
- "total_number_of_depreciations": 3,
- "frequency_of_depreciation": 10,
- "depreciation_start_date": month_end_date,
+ "total_number_of_depreciations": 12,
+ "frequency_of_depreciation": 1,
+ "depreciation_start_date": "2023-01-31",
},
)
asset_doc.submit()
@@ -77,9 +74,15 @@
first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active")
self.assertEquals(first_asset_depr_schedule.status, "Active")
+ post_depreciation_entries(getdate("2023-08-21"))
+
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
+ asset=asset_doc.name,
+ current_asset_value=current_value,
+ new_asset_value=50000.0,
+ date="2023-08-21",
)
adj_doc.submit()
@@ -90,8 +93,8 @@
self.assertEquals(first_asset_depr_schedule.status, "Cancelled")
expected_gle = (
- ("_Test Accumulated Depreciations - _TC", 0.0, 50000.0),
- ("_Test Depreciations - _TC", 50000.0, 0.0),
+ ("_Test Accumulated Depreciations - _TC", 0.0, 4625.29),
+ ("_Test Depreciations - _TC", 4625.29, 0.0),
)
gle = frappe.db.sql(
@@ -103,6 +106,29 @@
self.assertSequenceEqual(gle, expected_gle)
+ 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", 8300.0, 73674.71],
+ ["2023-09-30", 8300.0, 81974.71],
+ ["2023-10-31", 8300.0, 90274.71],
+ ["2023-11-30", 8300.0, 98574.71],
+ ["2023-12-31", 8300.0, 106874.71],
+ ["2024-01-15", 8300.0, 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)
+
def make_asset_value_adjustment(**args):
args = frappe._dict(args)
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 7c33056..f6a1951 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -185,8 +185,7 @@
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,
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index 06b9d29..3576cd4 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -52,6 +52,7 @@
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 @@
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"""
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index 3edaffa..55c01e8 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -901,6 +901,71 @@
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
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index fbfc1ac..06dbd86 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -25,6 +25,7 @@
"col_break_email_1",
"html_llwp",
"send_attached_files",
+ "send_document_print",
"sec_break_email_2",
"message_for_supplier",
"terms_section_break",
@@ -283,13 +284,21 @@
"fieldname": "send_attached_files",
"fieldtype": "Check",
"label": "Send Attached Files"
+ },
+ {
+ "default": "0",
+ "description": "If enabled, a print of this document will be attached to each email",
+ "fieldname": "send_document_print",
+ "fieldtype": "Check",
+ "label": "Send Document Print",
+ "print_hide": 1
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-08-08 16:30:10.870429",
+ "modified": "2023-08-09 12:20:26.850623",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index 56840c1..6b39982 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -205,10 +205,24 @@
if preview:
return {"message": message, "subject": subject}
- attachments = None
+ attachments = []
if self.send_attached_files:
attachments = self.get_attachments()
+ if self.send_document_print:
+ supplier_language = frappe.db.get_value("Supplier", data.supplier, "language")
+ system_language = frappe.db.get_single_value("System Settings", "language")
+ attachments.append(
+ frappe.attach_print(
+ self.doctype,
+ self.name,
+ doc=self,
+ print_format=self.meta.default_print_format or "Standard",
+ lang=supplier_language or system_language,
+ letterhead=self.letter_head,
+ )
+ )
+
self.send_email(data, sender, subject, message, attachments)
def send_email(self, data, sender, subject, message, attachments):
@@ -218,7 +232,6 @@
recipients=data.email_id,
sender=sender,
attachments=attachments,
- print_format=self.meta.default_print_format or "Standard",
send_email=True,
doctype=self.doctype,
name=self.name,
diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
index 71019e8..a7e03c0 100644
--- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py
@@ -154,31 +154,35 @@
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 @@
& (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)
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 340ec01..955ebef 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -2418,6 +2418,9 @@
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 @@
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 @@
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 @@
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 @@
if (
rate_unchanged
and qty_unchanged
+ and fg_qty_unchanged
and conversion_factor_unchanged
and uom_unchanged
and date_unchanged
@@ -2917,6 +2944,17 @@
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 @@
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()
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 7b7c53e..b396b27 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -759,7 +759,7 @@
"company": self.company,
"supplier": self.supplier,
"purchase_date": self.posting_date,
- "calculate_depreciation": 1,
+ "calculate_depreciation": 0,
"purchase_receipt_amount": purchase_amount,
"gross_purchase_amount": purchase_amount,
"asset_quantity": row.qty if is_grouped_asset else 0,
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 173e812..165e17b 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -345,6 +345,8 @@
elif doctype == "Purchase Invoice":
# look for Print Heading "Debit Note"
doc.select_print_heading = frappe.get_cached_value("Print Heading", _("Debit Note"))
+ if source.tax_withholding_category:
+ doc.set_onload("supplier_tds", source.tax_withholding_category)
for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual":
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index d669abe..ae54b80 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -599,6 +599,7 @@
inspection_fieldname_map = {
"Purchase Receipt": "inspection_required_before_purchase",
"Purchase Invoice": "inspection_required_before_purchase",
+ "Subcontracting Receipt": "inspection_required_before_purchase",
"Sales Invoice": "inspection_required_before_delivery",
"Delivery Note": "inspection_required_before_delivery",
}
diff --git a/erpnext/controllers/tests/test_subcontracting_controller.py b/erpnext/controllers/tests/test_subcontracting_controller.py
index eeb35c4..6b61ae9 100644
--- a/erpnext/controllers/tests/test_subcontracting_controller.py
+++ b/erpnext/controllers/tests/test_subcontracting_controller.py
@@ -1090,7 +1090,7 @@
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 = [
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 6ef8297..0b485bb 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -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();
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 61d2ace..11d5f6a 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -237,14 +237,15 @@
deposit = abs(amount)
withdrawal = 0.0
- status = "Pending" if transaction["pending"] == "True" else "Settled"
+ status = "Pending" if transaction["pending"] == True else "Settled"
tags = []
- try:
- tags += transaction["category"]
- tags += [f'Plaid Cat. {transaction["category_id"]}']
- except KeyError:
- pass
+ if transaction["category"]:
+ try:
+ tags += transaction["category"]
+ tags += [f'Plaid Cat. {transaction["category_id"]}']
+ except KeyError:
+ pass
if not frappe.db.exists("Bank Transaction", dict(transaction_id=transaction["transaction_id"])):
try:
diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json
index d024022..e8d3542 100644
--- a/erpnext/manufacturing/doctype/bom/bom.json
+++ b/erpnext/manufacturing/doctype/bom/bom.json
@@ -78,6 +78,10 @@
"show_items",
"show_operations",
"web_long_description",
+ "reference_section",
+ "bom_creator",
+ "bom_creator_item",
+ "column_break_oxbz",
"amended_from",
"connections_tab"
],
@@ -233,7 +237,7 @@
"fieldname": "rm_cost_as_per",
"fieldtype": "Select",
"label": "Rate Of Materials Based On",
- "options": "Valuation Rate\nLast Purchase Rate\nPrice List"
+ "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual"
},
{
"allow_on_submit": 1,
@@ -599,6 +603,32 @@
"fieldname": "operating_cost_per_bom_quantity",
"fieldtype": "Currency",
"label": "Operating Cost Per BOM Quantity"
+ },
+ {
+ "fieldname": "reference_section",
+ "fieldtype": "Section Break",
+ "label": "Reference"
+ },
+ {
+ "fieldname": "bom_creator",
+ "fieldtype": "Link",
+ "label": "BOM Creator",
+ "no_copy": 1,
+ "options": "BOM Creator",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "bom_creator_item",
+ "fieldtype": "Data",
+ "label": "BOM Creator Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_oxbz",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-sitemap",
@@ -606,7 +636,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2023-04-06 12:47:58.514795",
+ "modified": "2023-08-07 11:38:08.152294",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "BOM",
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 8058a5f..0231668 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -206,6 +206,7 @@
def on_submit(self):
self.manage_default_bom()
+ self.update_bom_creator_status()
def on_cancel(self):
self.db_set("is_active", 0)
@@ -214,6 +215,23 @@
# check if used in any other bom
self.validate_bom_links()
self.manage_default_bom()
+ self.update_bom_creator_status()
+
+ def update_bom_creator_status(self):
+ if not self.bom_creator:
+ return
+
+ if self.bom_creator_item:
+ frappe.db.set_value(
+ "BOM Creator Item",
+ self.bom_creator_item,
+ "bom_created",
+ 1 if self.docstatus == 1 else 0,
+ update_modified=False,
+ )
+
+ doc = frappe.get_doc("BOM Creator", self.bom_creator)
+ doc.set_status(save=True)
def on_update_after_submit(self):
self.validate_bom_links()
@@ -662,18 +680,19 @@
for d in self.get("items"):
old_rate = d.rate
- d.rate = self.get_rm_rate(
- {
- "company": self.company,
- "item_code": d.item_code,
- "bom_no": d.bom_no,
- "qty": d.qty,
- "uom": d.uom,
- "stock_uom": d.stock_uom,
- "conversion_factor": d.conversion_factor,
- "sourced_by_supplier": d.sourced_by_supplier,
- }
- )
+ if self.rm_cost_as_per != "Manual":
+ d.rate = self.get_rm_rate(
+ {
+ "company": self.company,
+ "item_code": d.item_code,
+ "bom_no": d.bom_no,
+ "qty": d.qty,
+ "uom": d.uom,
+ "stock_uom": d.stock_uom,
+ "conversion_factor": d.conversion_factor,
+ "sourced_by_supplier": d.sourced_by_supplier,
+ }
+ )
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
@@ -964,7 +983,12 @@
.as_("valuation_rate")
)
.where((bin_table.item_code == item_code) & (wh_table.company == company))
- ).run(as_dict=True)[0]
+ )
+
+ if data.get("set_rate_based_on_warehouse") and data.get("warehouse"):
+ item_valuation = item_valuation.where(bin_table.warehouse == data.get("warehouse"))
+
+ item_valuation = item_valuation.run(as_dict=True)[0]
valuation_rate = item_valuation.get("valuation_rate")
diff --git a/erpnext/manufacturing/doctype/bom_creator/__init__.py b/erpnext/manufacturing/doctype/bom_creator/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
new file mode 100644
index 0000000..01dc89b
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.js
@@ -0,0 +1,201 @@
+// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+frappe.provide("erpnext.bom");
+
+frappe.ui.form.on("BOM Creator", {
+ setup(frm) {
+ frm.trigger("set_queries");
+ },
+
+ setup_bom_creator(frm) {
+ frm.dashboard.clear_comment();
+
+ if (!frm.is_new()) {
+ if ((!frappe.bom_configurator
+ || frappe.bom_configurator.bom_configurator !== frm.doc.name)) {
+ frm.trigger("build_tree");
+ }
+ } else {
+ let $parent = $(frm.fields_dict["bom_creator"].wrapper);
+ $parent.empty();
+ frm.trigger("make_new_entry");
+ }
+ },
+
+ build_tree(frm) {
+ let $parent = $(frm.fields_dict["bom_creator"].wrapper);
+ $parent.empty();
+ frm.toggle_enable("item_code", false);
+
+ frappe.require('bom_configurator.bundle.js').then(() => {
+ frappe.bom_configurator = new frappe.ui.BOMConfigurator({
+ wrapper: $parent,
+ page: $parent,
+ frm: frm,
+ bom_configurator: frm.doc.name,
+ });
+ });
+ },
+
+ make_new_entry(frm) {
+ let dialog = new frappe.ui.Dialog({
+ title: __("Multi-level BOM Creator"),
+ fields: [
+ {
+ label: __("Name"),
+ fieldtype: "Data",
+ fieldname: "name",
+ reqd: 1
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Company"),
+ fieldtype: "Link",
+ fieldname: "company",
+ options: "Company",
+ reqd: 1,
+ default: frappe.defaults.get_user_default("Company"),
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Item Code (Final Product)"),
+ fieldtype: "Link",
+ fieldname: "item_code",
+ options: "Item",
+ reqd: 1
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Quantity"),
+ fieldtype: "Float",
+ fieldname: "qty",
+ reqd: 1,
+ default: 1.0
+ },
+ { fieldtype: "Section Break" },
+ {
+ label: __("Currency"),
+ fieldtype: "Link",
+ fieldname: "currency",
+ options: "Currency",
+ reqd: 1,
+ default: frappe.defaults.get_global_default("currency")
+ },
+ { fieldtype: "Column Break" },
+ {
+ label: __("Conversion Rate"),
+ fieldtype: "Float",
+ fieldname: "conversion_rate",
+ reqd: 1,
+ default: 1.0
+ },
+ ],
+ primary_action_label: __("Create"),
+ primary_action: (values) => {
+ values.doctype = frm.doc.doctype;
+ frappe.db
+ .insert(values)
+ .then((doc) => {
+ frappe.set_route("Form", doc.doctype, doc.name);
+ });
+ }
+ })
+
+ dialog.show();
+ },
+
+ set_queries(frm) {
+ frm.set_query("bom_no", "items", function(doc, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ return {
+ filters: {
+ item: item.item_code,
+ }
+ }
+ });
+ },
+
+ refresh(frm) {
+ frm.trigger("setup_bom_creator");
+ frm.trigger("set_root_item");
+ frm.trigger("add_custom_buttons");
+ },
+
+ set_root_item(frm) {
+ if (frm.is_new() && frm.doc.items?.length) {
+ frappe.model.set_value(frm.doc.items[0].doctype,
+ frm.doc.items[0].name, "is_root", 1);
+ }
+ },
+
+ add_custom_buttons(frm) {
+ if (!frm.is_new()) {
+ frm.add_custom_button(__("Rebuild Tree"), () => {
+ frm.trigger("build_tree");
+ });
+ }
+ }
+});
+
+frappe.ui.form.on("BOM Creator Item", {
+ item_code(frm, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ if (item.item_code && item.is_root) {
+ frappe.model.set_value(cdt, cdn, "fg_item", item.item_code);
+ }
+ },
+
+ do_not_explode(frm, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ if (!item.do_not_explode) {
+ frm.call({
+ method: "get_default_bom",
+ doc: frm.doc,
+ args: {
+ item_code: item.item_code
+ },
+ callback(r) {
+ if (r.message) {
+ frappe.model.set_value(cdt, cdn, "bom_no", r.message);
+ }
+ }
+ })
+ } else {
+ frappe.model.set_value(cdt, cdn, "bom_no", "");
+ }
+ }
+});
+
+
+erpnext.bom.BomConfigurator = class BomConfigurator extends erpnext.TransactionController {
+ conversion_rate(doc) {
+ if(this.frm.doc.currency === this.get_company_currency()) {
+ this.frm.set_value("conversion_rate", 1.0);
+ } else {
+ erpnext.bom.update_cost(doc);
+ }
+ }
+
+ buying_price_list(doc) {
+ this.apply_price_list();
+ }
+
+ plc_conversion_rate(doc) {
+ if (!this.in_apply_price_list) {
+ this.apply_price_list(null, true);
+ }
+ }
+
+ conversion_factor(doc, cdt, cdn) {
+ if (frappe.meta.get_docfield(cdt, "stock_qty", cdn)) {
+ var item = frappe.get_doc(cdt, cdn);
+ frappe.model.round_floats_in(item, ["qty", "conversion_factor"]);
+ item.stock_qty = flt(item.qty * item.conversion_factor, precision("stock_qty", item));
+ refresh_field("stock_qty", item.name, item.parentfield);
+ this.toggle_conversion_factor(item);
+ this.frm.events.update_cost(this.frm);
+ }
+ }
+};
+
+extend_cscript(cur_frm.cscript, new erpnext.bom.BomConfigurator({frm: cur_frm}));
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.json b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
new file mode 100644
index 0000000..fb4c6c5
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.json
@@ -0,0 +1,330 @@
+{
+ "actions": [],
+ "allow_import": 1,
+ "autoname": "prompt",
+ "creation": "2023-07-18 14:56:34.477800",
+ "default_view": "List",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "tab_2_tab",
+ "bom_creator",
+ "details_tab",
+ "section_break_ylsl",
+ "item_code",
+ "item_name",
+ "item_group",
+ "column_break_ikj7",
+ "qty",
+ "project",
+ "uom",
+ "raw_materials_tab",
+ "currency_detail",
+ "rm_cost_as_per",
+ "set_rate_based_on_warehouse",
+ "buying_price_list",
+ "price_list_currency",
+ "plc_conversion_rate",
+ "column_break_ivyw",
+ "currency",
+ "conversion_rate",
+ "section_break_zcfg",
+ "default_warehouse",
+ "column_break_tzot",
+ "company",
+ "materials_section",
+ "items",
+ "costing_detail",
+ "raw_material_cost",
+ "remarks_tab",
+ "remarks",
+ "section_break_yixm",
+ "status",
+ "column_break_irab",
+ "error_log",
+ "connections_tab",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "remember_last_selected_value": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "currency_detail",
+ "fieldtype": "Section Break",
+ "label": "Costing"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "Valuation Rate",
+ "fieldname": "rm_cost_as_per",
+ "fieldtype": "Select",
+ "label": "Rate Of Materials Based On",
+ "options": "Valuation Rate\nLast Purchase Rate\nPrice List\nManual",
+ "reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per===\"Price List\"",
+ "fieldname": "buying_price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per=='Price List'",
+ "fieldname": "price_list_currency",
+ "fieldtype": "Link",
+ "label": "Price List Currency",
+ "options": "Currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "depends_on": "eval:doc.rm_cost_as_per=='Price List'",
+ "fieldname": "plc_conversion_rate",
+ "fieldtype": "Float",
+ "label": "Price List Exchange Rate"
+ },
+ {
+ "fieldname": "column_break_ivyw",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Currency",
+ "options": "Currency",
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "conversion_rate",
+ "fieldtype": "Float",
+ "label": "Conversion Rate",
+ "precision": "9"
+ },
+ {
+ "fieldname": "materials_section",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break"
+ },
+ {
+ "allow_bulk_edit": 1,
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "oldfieldname": "bom_materials",
+ "oldfieldtype": "Table",
+ "options": "BOM Creator Item"
+ },
+ {
+ "fieldname": "costing_detail",
+ "fieldtype": "Section Break",
+ "label": "Costing Details"
+ },
+ {
+ "fieldname": "raw_material_cost",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total Cost",
+ "no_copy": 1,
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "remarks",
+ "fieldtype": "Text Editor",
+ "label": "Remarks"
+ },
+ {
+ "fieldname": "column_break_ikj7",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "label": "Project",
+ "options": "Project"
+ },
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Finished Good",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "label": "Quantity",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "tab_2_tab",
+ "fieldtype": "Tab Break",
+ "label": "BOM Tree"
+ },
+ {
+ "fieldname": "details_tab",
+ "fieldtype": "Tab Break",
+ "label": "Final Product"
+ },
+ {
+ "fieldname": "raw_materials_tab",
+ "fieldtype": "Tab Break",
+ "label": "Sub Assemblies & Raw Materials"
+ },
+ {
+ "fieldname": "remarks_tab",
+ "fieldtype": "Tab Break",
+ "label": "Remarks"
+ },
+ {
+ "fieldname": "connections_tab",
+ "fieldtype": "Tab Break",
+ "label": "Connections",
+ "show_dashboard": 1
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "BOM Creator",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "section_break_zcfg",
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
+ },
+ {
+ "fieldname": "column_break_tzot",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "default_warehouse",
+ "fieldtype": "Link",
+ "label": "Default Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fieldname": "bom_creator",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "section_break_ylsl",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.rm_cost_as_per === \"Valuation Rate\"",
+ "fieldname": "set_rate_based_on_warehouse",
+ "fieldtype": "Check",
+ "label": "Set Valuation Rate Based on Source Warehouse"
+ },
+ {
+ "fieldname": "section_break_yixm",
+ "fieldtype": "Section Break"
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Draft\nSubmitted\nIn Progress\nCompleted\nFailed\nCancelled",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_irab",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "error_log",
+ "fieldtype": "Text",
+ "label": "Error Log",
+ "read_only": 1
+ }
+ ],
+ "icon": "fa fa-sitemap",
+ "is_submittable": 1,
+ "links": [
+ {
+ "link_doctype": "BOM",
+ "link_fieldname": "bom_creator"
+ }
+ ],
+ "modified": "2023-08-07 15:45:06.176313",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Creator",
+ "naming_rule": "Set by user",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "show_name_in_global_search": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": [],
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
new file mode 100644
index 0000000..999d610
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator.py
@@ -0,0 +1,424 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from collections import OrderedDict
+
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import flt
+
+from erpnext.manufacturing.doctype.bom.bom import get_bom_item_rate
+
+BOM_FIELDS = [
+ "company",
+ "rm_cost_as_per",
+ "project",
+ "currency",
+ "conversion_rate",
+ "buying_price_list",
+]
+
+BOM_ITEM_FIELDS = [
+ "item_code",
+ "qty",
+ "uom",
+ "rate",
+ "stock_qty",
+ "stock_uom",
+ "conversion_factor",
+ "do_not_explode",
+]
+
+
+class BOMCreator(Document):
+ def before_save(self):
+ self.set_status()
+ self.set_is_expandable()
+ self.set_conversion_factor()
+ self.set_reference_id()
+ self.set_rate_for_items()
+
+ def validate(self):
+ self.validate_items()
+
+ def validate_items(self):
+ for row in self.items:
+ if row.is_expandable and row.item_code == self.item_code:
+ frappe.throw(_("Item {0} cannot be added as a sub-assembly of itself").format(row.item_code))
+
+ def set_status(self, save=False):
+ self.status = {
+ 0: "Draft",
+ 1: "Submitted",
+ 2: "Cancelled",
+ }[self.docstatus]
+
+ self.set_status_completed()
+ if save:
+ self.db_set("status", self.status)
+
+ def set_status_completed(self):
+ if self.docstatus != 1:
+ return
+
+ has_completed = True
+ for row in self.items:
+ if row.is_expandable and not row.bom_created:
+ has_completed = False
+ break
+
+ if not frappe.get_cached_value(
+ "BOM", {"bom_creator": self.name, "item": self.item_code}, "name"
+ ):
+ has_completed = False
+
+ if has_completed:
+ self.status = "Completed"
+
+ def on_cancel(self):
+ self.set_status(True)
+
+ def set_conversion_factor(self):
+ for row in self.items:
+ row.conversion_factor = 1.0
+
+ def before_submit(self):
+ self.validate_fields()
+ self.set_status()
+
+ def set_reference_id(self):
+ parent_reference = {row.idx: row.name for row in self.items}
+
+ for row in self.items:
+ if row.fg_reference_id:
+ continue
+
+ if row.parent_row_no:
+ row.fg_reference_id = parent_reference.get(row.parent_row_no)
+
+ @frappe.whitelist()
+ def add_boms(self):
+ self.submit()
+
+ def set_rate_for_items(self):
+ if self.rm_cost_as_per == "Manual":
+ return
+
+ amount = self.get_raw_material_cost()
+ self.raw_material_cost = amount
+
+ def get_raw_material_cost(self, fg_reference_id=None, amount=0):
+ if not fg_reference_id:
+ fg_reference_id = self.name
+
+ for row in self.items:
+ if row.fg_reference_id != fg_reference_id:
+ continue
+
+ if not row.is_expandable:
+ row.rate = get_bom_item_rate(
+ {
+ "company": self.company,
+ "item_code": row.item_code,
+ "bom_no": "",
+ "qty": row.qty,
+ "uom": row.uom,
+ "stock_uom": row.stock_uom,
+ "conversion_factor": row.conversion_factor,
+ "sourced_by_supplier": row.sourced_by_supplier,
+ },
+ self,
+ )
+
+ row.amount = flt(row.rate) * flt(row.qty)
+
+ else:
+ row.amount = 0.0
+ row.amount = self.get_raw_material_cost(row.name, row.amount)
+ row.rate = flt(row.amount) / (flt(row.qty) * flt(row.conversion_factor))
+
+ amount += flt(row.amount)
+
+ return amount
+
+ def set_is_expandable(self):
+ fg_items = [row.fg_item for row in self.items if row.fg_item != self.item_code]
+ for row in self.items:
+ row.is_expandable = 0
+ if row.item_code in fg_items:
+ row.is_expandable = 1
+
+ def validate_fields(self):
+ fields = {
+ "items": "Items",
+ }
+
+ for field, label in fields.items():
+ if not self.get(field):
+ frappe.throw(_("Please set {0} in BOM Creator {1}").format(label, self.name))
+
+ def on_submit(self):
+ self.enqueue_create_boms()
+
+ def enqueue_create_boms(self):
+ frappe.enqueue(
+ self.create_boms,
+ queue="short",
+ timeout=600,
+ is_async=True,
+ )
+
+ frappe.msgprint(
+ _("BOMs creation has been enqueued, kindly check the status after some time"), alert=True
+ )
+
+ def create_boms(self):
+ """
+ Sample data structure of production_item_wise_rm
+ production_item_wise_rm = {
+ (fg_item_code, name): {
+ "items": [],
+ "bom_no": "",
+ "fg_item_data": {}
+ }
+ }
+ """
+
+ self.db_set("status", "In Progress")
+ production_item_wise_rm = OrderedDict({})
+ production_item_wise_rm.setdefault(
+ (self.item_code, self.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": self})
+ )
+
+ for row in self.items:
+ if row.is_expandable:
+ if (row.item_code, row.name) not in production_item_wise_rm:
+ production_item_wise_rm.setdefault(
+ (row.item_code, row.name), frappe._dict({"items": [], "bom_no": "", "fg_item_data": row})
+ )
+
+ production_item_wise_rm[(row.fg_item, row.fg_reference_id)]["items"].append(row)
+
+ reverse_tree = OrderedDict(reversed(list(production_item_wise_rm.items())))
+
+ try:
+ for d in reverse_tree:
+ fg_item_data = production_item_wise_rm.get(d).fg_item_data
+ self.create_bom(fg_item_data, production_item_wise_rm)
+
+ frappe.msgprint(_("BOMs created successfully"))
+ except Exception:
+ traceback = frappe.get_traceback()
+ self.db_set(
+ {
+ "status": "Failed",
+ "error_log": traceback,
+ }
+ )
+
+ frappe.msgprint(_("BOMs creation failed"))
+
+ def create_bom(self, row, production_item_wise_rm):
+ bom = frappe.new_doc("BOM")
+ bom.update(
+ {
+ "item": row.item_code,
+ "bom_type": "Production",
+ "quantity": row.qty,
+ "allow_alternative_item": 1,
+ "bom_creator": self.name,
+ "bom_creator_item": row.name if row.name != self.name else "",
+ "rm_cost_as_per": "Manual",
+ }
+ )
+
+ for field in BOM_FIELDS:
+ if self.get(field):
+ bom.set(field, self.get(field))
+
+ for item in production_item_wise_rm[(row.item_code, row.name)]["items"]:
+ bom_no = ""
+ item.do_not_explode = 1
+ if (item.item_code, item.name) in production_item_wise_rm:
+ bom_no = production_item_wise_rm.get((item.item_code, item.name)).bom_no
+ item.do_not_explode = 0
+
+ item_args = {}
+ for field in BOM_ITEM_FIELDS:
+ item_args[field] = item.get(field)
+
+ item_args.update(
+ {
+ "bom_no": bom_no,
+ "allow_alternative_item": 1,
+ "allow_scrap_items": 1,
+ "include_item_in_manufacturing": 1,
+ }
+ )
+
+ bom.append("items", item_args)
+
+ bom.save(ignore_permissions=True)
+ bom.submit()
+
+ production_item_wise_rm[(row.item_code, row.name)].bom_no = bom.name
+
+ @frappe.whitelist()
+ def get_default_bom(self, item_code) -> str:
+ return frappe.get_cached_value("Item", item_code, "default_bom")
+
+
+@frappe.whitelist()
+def get_children(doctype=None, parent=None, **kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ fields = [
+ "item_code as value",
+ "is_expandable as expandable",
+ "parent as parent_id",
+ "qty",
+ "idx",
+ "'BOM Creator Item' as doctype",
+ "name",
+ "uom",
+ "rate",
+ "amount",
+ ]
+
+ query_filters = {
+ "fg_item": parent,
+ "parent": kwargs.parent_id,
+ }
+
+ if kwargs.name:
+ query_filters["name"] = kwargs.name
+
+ return frappe.get_all("BOM Creator Item", fields=fields, filters=query_filters, order_by="idx")
+
+
+@frappe.whitelist()
+def add_item(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ item_info = get_item_details(kwargs.item_code)
+ kwargs.update(
+ {
+ "uom": item_info.stock_uom,
+ "stock_uom": item_info.stock_uom,
+ "conversion_factor": 1,
+ }
+ )
+
+ doc.append("items", kwargs)
+ doc.save()
+
+ return doc
+
+
+@frappe.whitelist()
+def add_sub_assembly(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ bom_item = frappe.parse_json(kwargs.bom_item)
+
+ name = kwargs.fg_reference_id
+ parent_row_no = ""
+ if not kwargs.convert_to_sub_assembly:
+ item_info = get_item_details(bom_item.item_code)
+ item_row = doc.append(
+ "items",
+ {
+ "item_code": bom_item.item_code,
+ "qty": bom_item.qty,
+ "uom": item_info.stock_uom,
+ "fg_item": kwargs.fg_item,
+ "conversion_factor": 1,
+ "fg_reference_id": name,
+ "stock_qty": bom_item.qty,
+ "fg_reference_id": name,
+ "do_not_explode": 1,
+ "is_expandable": 1,
+ "stock_uom": item_info.stock_uom,
+ },
+ )
+
+ parent_row_no = item_row.idx
+ name = ""
+
+ for row in bom_item.get("items"):
+ row = frappe._dict(row)
+ item_info = get_item_details(row.item_code)
+ doc.append(
+ "items",
+ {
+ "item_code": row.item_code,
+ "qty": row.qty,
+ "fg_item": bom_item.item_code,
+ "uom": item_info.stock_uom,
+ "fg_reference_id": name,
+ "parent_row_no": parent_row_no,
+ "conversion_factor": 1,
+ "do_not_explode": 1,
+ "stock_qty": row.qty,
+ "stock_uom": item_info.stock_uom,
+ },
+ )
+
+ doc.save()
+
+ return doc
+
+
+def get_item_details(item_code):
+ return frappe.get_cached_value(
+ "Item", item_code, ["item_name", "description", "image", "stock_uom", "default_bom"], as_dict=1
+ )
+
+
+@frappe.whitelist()
+def delete_node(**kwargs):
+ if isinstance(kwargs, str):
+ kwargs = frappe.parse_json(kwargs)
+
+ if isinstance(kwargs, dict):
+ kwargs = frappe._dict(kwargs)
+
+ items = get_children(parent=kwargs.fg_item, parent_id=kwargs.parent)
+ if kwargs.docname:
+ frappe.delete_doc("BOM Creator Item", kwargs.docname)
+
+ for item in items:
+ frappe.delete_doc("BOM Creator Item", item.name)
+ if item.expandable:
+ delete_node(fg_item=item.value, parent=item.parent_id)
+
+ doc = frappe.get_doc("BOM Creator", kwargs.parent)
+ doc.set_rate_for_items()
+ doc.save()
+
+ return doc
+
+
+@frappe.whitelist()
+def edit_qty(doctype, docname, qty, parent):
+ frappe.db.set_value(doctype, docname, "qty", qty)
+ doc = frappe.get_doc("BOM Creator", parent)
+ doc.set_rate_for_items()
+ doc.save()
+
+ return doc
diff --git a/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js
new file mode 100644
index 0000000..423b721
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/bom_creator_list.js
@@ -0,0 +1,18 @@
+frappe.listview_settings['BOM Creator'] = {
+ add_fields: ["status"],
+ get_indicator: function (doc) {
+ if (doc.status === "Draft") {
+ return [__("Draft"), "red", "status,=,Draft"];
+ } else if (doc.status === "In Progress") {
+ return [__("In Progress"), "orange", "status,=,In Progress"];
+ } else if (doc.status === "Completed") {
+ return [__("Completed"), "green", "status,=,Completed"];
+ } else if (doc.status === "Cancelled") {
+ return [__("Cancelled"), "red", "status,=,Cancelled"];
+ } else if (doc.status === "Failed") {
+ return [__("Failed"), "red", "status,=,Failed"];
+ } else if (doc.status === "Submitted") {
+ return [__("Submitted"), "blue", "status,=,Submitted"];
+ }
+ },
+};
diff --git a/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
new file mode 100644
index 0000000..d239d58
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator/test_bom_creator.py
@@ -0,0 +1,240 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+
+import random
+
+import frappe
+from frappe.tests.utils import FrappeTestCase
+
+from erpnext.manufacturing.doctype.bom_creator.bom_creator import (
+ add_item,
+ add_sub_assembly,
+ delete_node,
+ edit_qty,
+)
+from erpnext.stock.doctype.item.test_item import make_item
+
+
+class TestBOMCreator(FrappeTestCase):
+ def setUp(self) -> None:
+ create_items()
+
+ def test_bom_sub_assembly(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM with Sub Assembly",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_sub_assembly(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ bom_item={
+ "item_code": "Frame Assembly",
+ "qty": 1,
+ "items": [
+ {
+ "item_code": "Frame",
+ "qty": 1,
+ },
+ {
+ "item_code": "Fork",
+ "qty": 1,
+ },
+ ],
+ },
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].item_code, "Frame Assembly")
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Frame Assembly")
+ self.assertEqual(row.fg_reference_id, doc.items[0].name)
+
+ self.assertEqual(doc.items[0].amount, fg_valuation_rate)
+
+ def test_bom_raw_material(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM with Raw Material",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_item(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ item_code="Pedal Assembly",
+ qty=2,
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].item_code, "Pedal Assembly")
+ self.assertEqual(doc.items[0].qty, 2)
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Bicycle")
+ self.assertEqual(row.fg_reference_id, doc.name)
+
+ self.assertEqual(doc.raw_material_cost, fg_valuation_rate)
+
+ def test_convert_to_sub_assembly(self):
+ final_product = "Bicycle"
+ make_item(
+ final_product,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+ doc = make_bom_creator(
+ name="Bicycle BOM",
+ company="_Test Company",
+ item_code=final_product,
+ qty=1,
+ rm_cosy_as_per="Valuation Rate",
+ currency="INR",
+ plc_conversion_rate=1,
+ conversion_rate=1,
+ )
+
+ add_item(
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.name,
+ item_code="Pedal Assembly",
+ qty=2,
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].is_expandable, 0)
+
+ add_sub_assembly(
+ convert_to_sub_assembly=1,
+ parent=doc.name,
+ fg_item=final_product,
+ fg_reference_id=doc.items[0].name,
+ bom_item={
+ "item_code": "Pedal Assembly",
+ "qty": 2,
+ "items": [
+ {
+ "item_code": "Pedal Body",
+ "qty": 2,
+ },
+ {
+ "item_code": "Pedal Axle",
+ "qty": 2,
+ },
+ ],
+ },
+ )
+
+ doc.reload()
+ self.assertEqual(doc.items[0].is_expandable, 1)
+
+ fg_valuation_rate = 0
+ for row in doc.items:
+ if not row.is_expandable:
+ fg_valuation_rate += row.amount
+ self.assertEqual(row.fg_item, "Pedal Assembly")
+ self.assertEqual(row.qty, 2.0)
+ self.assertEqual(row.fg_reference_id, doc.items[0].name)
+
+ self.assertEqual(doc.raw_material_cost, fg_valuation_rate)
+
+
+def create_items():
+ raw_materials = [
+ "Frame",
+ "Fork",
+ "Rim",
+ "Spokes",
+ "Hub",
+ "Tube",
+ "Tire",
+ "Pedal Body",
+ "Pedal Axle",
+ "Ball Bearings",
+ "Chain Links",
+ "Chain Pins",
+ "Seat",
+ "Seat Post",
+ "Seat Clamp",
+ ]
+
+ for item in raw_materials:
+ valuation_rate = random.choice([100, 200, 300, 500, 333, 222, 44, 20, 10])
+ make_item(
+ item,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ "valuation_rate": valuation_rate,
+ },
+ )
+
+ sub_assemblies = [
+ "Frame Assembly",
+ "Wheel Assembly",
+ "Pedal Assembly",
+ "Chain Assembly",
+ "Seat Assembly",
+ ]
+
+ for item in sub_assemblies:
+ make_item(
+ item,
+ {
+ "item_group": "Raw Material",
+ "stock_uom": "Nos",
+ },
+ )
+
+
+def make_bom_creator(**kwargs):
+ if isinstance(kwargs, str) or isinstance(kwargs, dict):
+ kwargs = frappe.parse_json(kwargs)
+
+ doc = frappe.new_doc("BOM Creator")
+ doc.update(kwargs)
+ doc.save()
+
+ return doc
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/__init__.py b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/__init__.py
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
new file mode 100644
index 0000000..fdb5d3a
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.json
@@ -0,0 +1,243 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2023-07-18 14:35:50.307386",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "item_name",
+ "item_group",
+ "column_break_f63f",
+ "fg_item",
+ "source_warehouse",
+ "is_expandable",
+ "sourced_by_supplier",
+ "bom_created",
+ "description_section",
+ "description",
+ "quantity_and_rate_section",
+ "qty",
+ "rate",
+ "uom",
+ "column_break_bgnb",
+ "stock_qty",
+ "conversion_factor",
+ "stock_uom",
+ "amount_section",
+ "amount",
+ "column_break_yuca",
+ "base_rate",
+ "base_amount",
+ "section_break_wtld",
+ "do_not_explode",
+ "parent_row_no",
+ "fg_reference_id",
+ "column_break_sulm",
+ "instruction"
+ ],
+ "fields": [
+ {
+ "columns": 2,
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fetch_if_empty": 1,
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group"
+ },
+ {
+ "fieldname": "column_break_f63f",
+ "fieldtype": "Column Break"
+ },
+ {
+ "columns": 2,
+ "fieldname": "fg_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "FG Item",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_expandable",
+ "fieldtype": "Check",
+ "label": "Is Expandable",
+ "read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "description_section",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "fetch_from": "item_code.description",
+ "fetch_if_empty": 1,
+ "fieldname": "description",
+ "fieldtype": "Small Text"
+ },
+ {
+ "fieldname": "quantity_and_rate_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity and Rate"
+ },
+ {
+ "columns": 1,
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Qty"
+ },
+ {
+ "columns": 2,
+ "fieldname": "rate",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Rate"
+ },
+ {
+ "columns": 1,
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "column_break_bgnb",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "stock_qty",
+ "fieldtype": "Float",
+ "label": "Stock Qty",
+ "read_only": 1
+ },
+ {
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "Conversion Factor"
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "no_copy": 1,
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amount_section",
+ "fieldtype": "Section Break",
+ "label": "Amount"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_yuca",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "1",
+ "fieldname": "do_not_explode",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Do Not Explode"
+ },
+ {
+ "fieldname": "instruction",
+ "fieldtype": "Small Text",
+ "label": "Instruction"
+ },
+ {
+ "fieldname": "base_amount",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Base Amount"
+ },
+ {
+ "fieldname": "base_rate",
+ "fieldtype": "Currency",
+ "hidden": 1,
+ "label": "Base Rate"
+ },
+ {
+ "default": "0",
+ "fieldname": "sourced_by_supplier",
+ "fieldtype": "Check",
+ "label": "Sourced by Supplier"
+ },
+ {
+ "fieldname": "section_break_wtld",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "fg_reference_id",
+ "fieldtype": "Data",
+ "label": "FG Reference",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_sulm",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "parent_row_no",
+ "fieldtype": "Data",
+ "label": "Parent Row No",
+ "no_copy": 1,
+ "print_hide": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "bom_created",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "BOM Created",
+ "no_copy": 1,
+ "print_hide": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2023-08-07 11:52:30.492233",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "BOM Creator Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
new file mode 100644
index 0000000..350c918
--- /dev/null
+++ b/erpnext/manufacturing/doctype/bom_creator_item/bom_creator_item.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class BOMCreatorItem(Document):
+ pass
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 261aa76..131f438 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -347,7 +347,7 @@
if not data.pending_qty:
continue
- item_details = get_item_details(data.item_code)
+ item_details = get_item_details(data.item_code, throw=False)
if self.combine_items:
if item_details.bom_no in refs:
refs[item_details.bom_no]["so_details"].append(
@@ -795,6 +795,9 @@
if not row.item_code:
frappe.throw(_("Row #{0}: Please select Item Code in Assembly Items").format(row.idx))
+ if not row.bom_no:
+ frappe.throw(_("Row #{0}: Please select the BOM No in Assembly Items").format(row.idx))
+
bom_data = []
warehouse = row.warehouse if self.skip_available_sub_assembly_item else None
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index 7c15bf9..5ad79f9 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -1082,7 +1082,7 @@
@frappe.whitelist()
-def get_item_details(item, project=None, skip_bom_info=False):
+def get_item_details(item, project=None, skip_bom_info=False, throw=True):
res = frappe.db.sql(
"""
select stock_uom, description, item_name, allow_alternative_item,
@@ -1118,12 +1118,15 @@
if not res["bom_no"]:
if project:
- res = get_item_details(item)
+ res = get_item_details(item, throw=throw)
frappe.msgprint(
_("Default BOM not found for Item {0} and Project {1}").format(item, project), alert=1
)
else:
- frappe.throw(_("Default BOM for {0} not found").format(item))
+ msg = _("Default BOM for {0} not found").format(item)
+ frappe.msgprint(msg, raise_exception=throw, indicator="yellow", alert=(not throw))
+
+ return res
bom_data = frappe.db.get_value(
"BOM",
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index 518ae14..8e07850 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"id\":\"csBCiDglCE\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"YHCQG3wAGv\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Creator\",\"col\":3}},{\"id\":\"xit0dg7KvY\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM\",\"col\":3}},{\"id\":\"LRhGV9GAov\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Plan\",\"col\":3}},{\"id\":\"69KKosI6Hg\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Work Order\",\"col\":3}},{\"id\":\"PwndxuIpB3\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Card\",\"col\":3}},{\"id\":\"OaiDqTT03Y\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Forecasting\",\"col\":3}},{\"id\":\"OtMcArFRa5\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"BOM Stock Report\",\"col\":3}},{\"id\":\"76yYsI5imF\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Production Planning Report\",\"col\":3}},{\"id\":\"PIQJYZOMnD\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Learn Manufacturing\",\"col\":3}},{\"id\":\"bN_6tHS-Ct\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yVEFZMqVwd\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"id\":\"rwrmsTI58-\",\"type\":\"card\",\"data\":{\"card_name\":\"Production\",\"col\":4}},{\"id\":\"6dnsyX-siZ\",\"type\":\"card\",\"data\":{\"card_name\":\"Bill of Materials\",\"col\":4}},{\"id\":\"CIq-v5f5KC\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"id\":\"8RRiQeYr0G\",\"type\":\"card\",\"data\":{\"card_name\":\"Tools\",\"col\":4}},{\"id\":\"Pu8z7-82rT\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
"creation": "2020-03-02 17:11:37.032604",
"custom_blocks": [],
"docstatus": 0,
@@ -316,7 +316,7 @@
"type": "Link"
}
],
- "modified": "2023-07-04 14:40:47.281125",
+ "modified": "2023-08-08 22:28:39.633891",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
@@ -339,6 +339,13 @@
{
"color": "Grey",
"doc_view": "List",
+ "label": "BOM Creator",
+ "link_to": "BOM Creator",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "List",
"label": "BOM",
"link_to": "BOM",
"stats_filter": "{\"is_active\":[\"=\",1]}",
diff --git a/erpnext/public/js/bom_configurator/bom_configurator.bundle.js b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
new file mode 100644
index 0000000..b3b2e9f
--- /dev/null
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -0,0 +1,416 @@
+class BOMConfigurator {
+ constructor({ wrapper, page, frm, bom_configurator }) {
+ this.$wrapper = $(wrapper);
+ this.page = page;
+ this.bom_configurator = bom_configurator;
+ this.frm = frm;
+
+ this.make();
+ this.prepare_layout();
+ this.bind_events();
+ }
+
+ add_boms() {
+ this.frm.call({
+ method: "add_boms",
+ freeze: true,
+ doc: this.frm.doc,
+ });
+ }
+
+ make() {
+ let options = {
+ ...this.tree_options(),
+ ...this.tree_methods(),
+ };
+
+ frappe.views.trees["BOM Configurator"] = new frappe.views.TreeView(options);
+ this.tree_view = frappe.views.trees["BOM Configurator"];
+ }
+
+ bind_events() {
+ frappe.views.trees["BOM Configurator"].events = {
+ frm: this.frm,
+ add_item: this.add_item,
+ add_sub_assembly: this.add_sub_assembly,
+ get_sub_assembly_modal_fields: this.get_sub_assembly_modal_fields,
+ convert_to_sub_assembly: this.convert_to_sub_assembly,
+ delete_node: this.delete_node,
+ edit_qty: this.edit_qty,
+ load_tree: this.load_tree,
+ set_default_qty: this.set_default_qty,
+ }
+ }
+
+ tree_options() {
+ return {
+ parent: this.$wrapper.get(0),
+ body: this.$wrapper.get(0),
+ doctype: 'BOM Configurator',
+ page: this.page,
+ expandable: true,
+ title: __("Configure Product Assembly"),
+ breadcrumb: "Manufacturing",
+ get_tree_nodes: "erpnext.manufacturing.doctype.bom_creator.bom_creator.get_children",
+ root_label: this.frm.doc.item_code,
+ disable_add_node: true,
+ get_tree_root: false,
+ show_expand_all: false,
+ extend_toolbar: false,
+ do_not_make_page: true,
+ do_not_setup_menu: true,
+ }
+ }
+
+ tree_methods() {
+ let frm_obj = this;
+ let view = frappe.views.trees["BOM Configurator"];
+
+ return {
+ onload: function(me) {
+ me.args["parent_id"] = frm_obj.frm.doc.name;
+ me.args["parent"] = frm_obj.frm.doc.item_code;
+ me.parent = frm_obj.$wrapper.get(0);
+ me.body = frm_obj.$wrapper.get(0);
+ me.make_tree();
+ },
+ onrender(node) {
+ const qty = node.data.qty || frm_obj.frm.doc.qty;
+ const uom = node.data.uom || frm_obj.frm.doc.uom;
+ const docname = node.data.name || frm_obj.frm.doc.name;
+ let amount = node.data.amount;
+ if (node.data.value === frm_obj.frm.doc.item_code) {
+ amount = frm_obj.frm.doc.raw_material_cost;
+ }
+
+ amount = frappe.format(amount, { fieldtype: "Currency", currency: frm_obj.frm.doc.currency });
+
+ $(`
+ <div class="pill small pull-right bom-qty-pill"
+ style="background-color: var(--bg-white);
+ color: var(--text-on-gray);
+ font-weight:450;
+ margin-right: 40px;
+ display: inline-flex;
+ min-width: 128px;
+ border: 1px solid var(--bg-gray);
+ ">
+ <div style="padding-right:5px" data-bom-qty-docname="${docname}">${qty} ${uom}</div>
+ <div class="fg-item-amt" style="padding-left:12px; border-left:1px solid var(--bg-gray)">
+ ${amount}
+ </div>
+ </div>
+
+ `).insertBefore(node.$ul);
+ },
+ toolbar: this.frm?.doc.docstatus === 0 ? [
+ {
+ label:__(frappe.utils.icon('edit', 'sm') + " Qty"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.edit_qty(node, view);
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('add', 'sm') + " Raw Material"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.add_item(node, view);
+ },
+ condition: function(node) {
+ return node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('add', 'sm') + " Sub Assembly"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.add_sub_assembly(node, view);
+ },
+ condition: function(node) {
+ return node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__("Expand All"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+
+ if (!node.expanded) {
+ view.tree.load_children(node, true);
+ $(node.parent[0]).find(".tree-children").show();
+ node.$toolbar.find(".expand-all-btn").html("Collapse All");
+ } else {
+ node.$tree_link.trigger("click");
+ node.$toolbar.find(".expand-all-btn").html("Expand All");
+ }
+ },
+ condition: function(node) {
+ return node.expandable && node.is_root;
+ },
+ btnClass: "hidden-xs expand-all-btn"
+ },
+ {
+ label:__(frappe.utils.icon('move', 'sm') + " Sub Assembly"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.convert_to_sub_assembly(node, view);
+ },
+ condition: function(node) {
+ return !node.expandable;
+ },
+ btnClass: "hidden-xs"
+ },
+ {
+ label:__(frappe.utils.icon('delete', 'sm') + __(" Item")),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+ view.events.delete_node(node, view);
+ },
+ condition: function(node) {
+ return !node.is_root;
+ },
+ btnClass: "hidden-xs"
+ },
+ ] : [{
+ label:__("Expand All"),
+ click: function(node) {
+ let view = frappe.views.trees["BOM Configurator"];
+
+ if (!node.expanded) {
+ view.tree.load_children(node, true);
+ $(node.parent[0]).find(".tree-children").show();
+ node.$toolbar.find(".expand-all-btn").html("Collapse All");
+ } else {
+ node.$tree_link.trigger("click");
+ node.$toolbar.find(".expand-all-btn").html("Expand All");
+ }
+ },
+ condition: function(node) {
+ return node.expandable && node.is_root;
+ },
+ btnClass: "hidden-xs expand-all-btn"
+ }],
+ }
+ }
+
+ add_item(node, view) {
+ frappe.prompt([
+ { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1 },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1 },
+ ],
+ (data) => {
+ if (!node.data.parent_id) {
+ node.data.parent_id = this.frm.doc.name;
+ }
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_item",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ item_code: data.item_code,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ qty: data.qty,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node);
+ }
+ });
+ },
+ __("Add Item"),
+ __("Add"));
+ }
+
+ add_sub_assembly(node, view) {
+ let dialog = new frappe.ui.Dialog({
+ fields: view.events.get_sub_assembly_modal_fields(),
+ title: __("Add Sub Assembly"),
+ });
+
+ dialog.show();
+ view.events.set_default_qty(dialog);
+
+ dialog.set_primary_action(__("Add"), () => {
+ let bom_item = dialog.get_values();
+
+ if (!node.data?.parent_id) {
+ node.data.parent_id = this.frm.doc.name;
+ }
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ bom_item: bom_item,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node);
+ }
+ });
+
+ dialog.hide();
+ });
+
+ }
+
+ get_sub_assembly_modal_fields(read_only=false) {
+ return [
+ { label: __("Sub Assembly Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, read_only: read_only },
+ { fieldtype: "Column Break" },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, read_only: read_only },
+ { fieldtype: "Section Break" },
+ { label: __("Raw Materials"), fieldname: "items", fieldtype: "Table", reqd: 1,
+ fields: [
+ { label: __("Item"), fieldname: "item_code", fieldtype: "Link", options: "Item", reqd: 1, in_list_view: 1 },
+ { label: __("Qty"), fieldname: "qty", default: 1.0, fieldtype: "Float", reqd: 1, in_list_view: 1 },
+ ]
+ },
+ ]
+ }
+
+ convert_to_sub_assembly(node, view) {
+ let dialog = new frappe.ui.Dialog({
+ fields: view.events.get_sub_assembly_modal_fields(true),
+ title: __("Add Sub Assembly"),
+ });
+
+ dialog.set_values({
+ item_code: node.data.value,
+ qty: node.data.qty,
+ });
+
+ dialog.show();
+ view.events.set_default_qty(dialog);
+
+ dialog.set_primary_action(__("Add"), () => {
+ let bom_item = dialog.get_values();
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.add_sub_assembly",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ bom_item: bom_item,
+ fg_reference_id: node.data.name || this.frm.doc.name,
+ convert_to_sub_assembly: true,
+ },
+ callback: (r) => {
+ node.expandable = true;
+ view.events.load_tree(r, node);
+ }
+ });
+
+ dialog.hide();
+ });
+ }
+
+ set_default_qty(dialog) {
+ dialog.fields_dict.items.grid.fields_map.item_code.onchange = function (event) {
+ if (event) {
+ let name = $(event.currentTarget).closest('.grid-row').attr("data-name")
+ let item_row = dialog.fields_dict.items.grid.grid_rows_by_docname[name].doc;
+ item_row.qty = 1;
+ dialog.fields_dict.items.grid.refresh()
+ }
+ }
+ }
+
+ delete_node(node, view) {
+ frappe.confirm(__("Are you sure you want to delete this Item?"), () => {
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.delete_node",
+ args: {
+ parent: node.data.parent_id,
+ fg_item: node.data.value,
+ doctype: node.data.doctype,
+ docname: node.data.name,
+ },
+ callback: (r) => {
+ view.events.load_tree(r, node.parent_node);
+ }
+ });
+ });
+ }
+
+ edit_qty(node, view) {
+ let qty = node.data.qty || this.frm.doc.qty;
+ frappe.prompt([
+ { label: __("Qty"), fieldname: "qty", default: qty, fieldtype: "Float", reqd: 1 },
+ ],
+ (data) => {
+ let doctype = node.data.doctype || this.frm.doc.doctype;
+ let docname = node.data.name || this.frm.doc.name;
+
+ frappe.call({
+ method: "erpnext.manufacturing.doctype.bom_creator.bom_creator.edit_qty",
+ args: {
+ doctype: doctype,
+ docname: docname,
+ qty: data.qty,
+ parent: node.data.parent_id,
+ },
+ callback: (r) => {
+ node.data.qty = data.qty;
+ let uom = node.data.uom || this.frm.doc.uom;
+ $(node.parent.get(0)).find(`[data-bom-qty-docname='${docname}']`).html(data.qty + " " + uom);
+ view.events.load_tree(r, node);
+ }
+ });
+ },
+ __("Edit Qty"),
+ __("Update"));
+ }
+
+ prepare_layout() {
+ let main_div = $(this.page)[0];
+
+ main_div.style.marginBottom = "15px";
+ $(main_div).find(".tree-children")[0].style.minHeight = "370px";
+ $(main_div).find(".tree-children")[0].style.maxHeight = "370px";
+ $(main_div).find(".tree-children")[0].style.overflowY = "auto";
+ }
+
+ load_tree(response, node) {
+ let item_row = "";
+ let parent_dom = ""
+ let total_amount = response.message.raw_material_cost;
+
+ frappe.views.trees["BOM Configurator"].tree.load_children(node);
+
+ while (true) {
+ item_row = response.message.items.filter(item => item.name === node.data.name);
+
+ if (item_row?.length) {
+ node.data.amount = item_row[0].amount;
+ total_amount = node.data.amount
+ } else {
+ total_amount = response.message.raw_material_cost;
+ }
+
+ parent_dom = $(node.parent.get(0));
+ total_amount = frappe.format(
+ total_amount, {
+ fieldtype: "Currency",
+ currency: this.frm.doc.currency
+ }
+ );
+
+ $($(parent_dom).find(".fg-item-amt")[0]).html(total_amount);
+
+ if (node.is_root) {
+ break;
+ }
+
+ node = node.parent_node;
+ }
+
+ }
+}
+
+frappe.ui.BOMConfigurator = BOMConfigurator;
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 59d2b15..ac5735b 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -277,7 +277,7 @@
}
setup_quality_inspection() {
- if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)) {
+ if(!in_list(["Delivery Note", "Sales Invoice", "Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)) {
return;
}
@@ -289,7 +289,7 @@
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
}
- const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice"], this.frm.doc.doctype)
+ const inspection_type = in_list(["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"], this.frm.doc.doctype)
? "Incoming" : "Outgoing";
let quality_inspection_field = this.frm.get_docfield("items", "quality_inspection");
@@ -2067,6 +2067,7 @@
const me = this;
const dialog = new frappe.ui.Dialog({
title: __("Select Items for Quality Inspection"),
+ size: "extra-large",
fields: fields,
primary_action: function () {
const data = dialog.get_values();
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index ba200ef..3cbec3e 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -45,7 +45,8 @@
fieldname: 'setup_demo',
label: __('Generate Demo Data for Exploration'),
fieldtype: 'Check',
- description: 'If checked, we will create demo data for you to explore the system. This demo data can be erased later.'},
+ description: __('If checked, we will create demo data for you to explore the system. This demo data can be erased later.')
+ },
],
onload: function (slide) {
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index f456e5e..a3c10c6 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -114,6 +114,10 @@
},
view_serial_batch_nos: function(frm) {
+ if (!frm.doc?.items) {
+ return;
+ }
+
let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle);
if (bundle_ids?.length) {
@@ -579,7 +583,9 @@
"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 +684,37 @@
})
}
+ 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",
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 555db59..d351c3c 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -15,6 +15,7 @@
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from frappe.model.utils.rename_doc import update_linked_doctypes
from frappe.utils import cint, cstr, flt, get_formatted_email, today
+from frappe.utils.nestedset import get_root_of
from frappe.utils.user import get_users_with_role
from erpnext.accounts.party import ( # noqa
@@ -80,6 +81,7 @@
validate_party_accounts(self)
self.validate_credit_limit_on_change()
self.set_loyalty_program()
+ self.set_territory_and_group()
self.check_customer_group_change()
self.validate_default_bank_account()
self.validate_internal_customer()
@@ -138,6 +140,12 @@
_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account))
)
+ def set_territory_and_group(self):
+ if not self.territory:
+ self.territory = get_root_of("Territory")
+ if not self.customer_group:
+ self.customer_group = get_root_of("Customer Group")
+
def validate_internal_customer(self):
if not self.is_internal_customer:
self.represents_company = ""
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index be75bd6..d341d23 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -48,8 +48,8 @@
const email_dialog = new frappe.ui.Dialog({
title: 'Email Receipt',
fields: [
- {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'},
- // {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'}
+ {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID', reqd: 1},
+ {fieldname:'content', fieldtype:'Small Text', label:'Message (if any)'}
],
primary_action: () => {
this.send_email();
@@ -243,6 +243,7 @@
send_email() {
const frm = this.events.get_frm();
const recipients = this.email_dialog.get_values().email_id;
+ const content = this.email_dialog.get_values().content;
const doc = this.doc || frm.doc;
const print_format = frm.pos_print_format;
@@ -251,6 +252,7 @@
args: {
recipients: recipients,
subject: __(frm.meta.name) + ': ' + doc.name,
+ content: content ? content : __(frm.meta.name) + ': ' + doc.name,
doctype: doc.doctype,
name: doc.name,
send_email: 1,
diff --git a/erpnext/setup/demo_data/customer.json b/erpnext/setup/demo_data/customer.json
index 1b47906..5e77e78 100644
--- a/erpnext/setup/demo_data/customer.json
+++ b/erpnext/setup/demo_data/customer.json
@@ -2,19 +2,16 @@
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
- "territory": "All Territories",
"customer_name": "Grant Plastics Ltd."
},
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
- "territory": "All Territories",
"customer_name": "West View Software Ltd."
},
{
"doctype": "Customer",
"customer_group": "Demo Customer Group",
- "territory": "All Territories",
"customer_name": "Palmer Productions Ltd."
}
-]
\ No newline at end of file
+]
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index fcdf245..b05696a 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -403,14 +403,20 @@
self._set_default_account(default_account, default_accounts.get(default_account))
if not self.default_income_account:
- income_account = frappe.db.get_value(
- "Account", {"account_name": _("Sales"), "company": self.name, "is_group": 0}
+ income_account = frappe.db.get_all(
+ "Account",
+ filters={"company": self.name, "is_group": 0},
+ or_filters={
+ "account_name": ("in", [_("Sales"), _("Sales Account")]),
+ "account_type": "Income Account",
+ },
+ pluck="name",
)
- if not income_account:
- income_account = frappe.db.get_value(
- "Account", {"account_name": _("Sales Account"), "company": self.name}
- )
+ if income_account:
+ income_account = income_account[0]
+ else:
+ income_account = None
self.db_set("default_income_account", income_account)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index 1139c4b..9efae6a 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -656,7 +656,10 @@
"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,
diff --git a/erpnext/stock/doctype/material_request/material_request_dashboard.py b/erpnext/stock/doctype/material_request/material_request_dashboard.py
index 2bba52a..f91ea6a 100644
--- a/erpnext/stock/doctype/material_request/material_request_dashboard.py
+++ b/erpnext/stock/doctype/material_request/material_request_dashboard.py
@@ -6,6 +6,8 @@
"fieldname": "material_request",
"internal_links": {
"Sales Order": ["items", "sales_order"],
+ "Project": ["items", "project"],
+ "Cost Center": ["items", "cost_center"],
},
"transactions": [
{
@@ -15,5 +17,6 @@
{"label": _("Stock"), "items": ["Stock Entry", "Purchase Receipt", "Pick List"]},
{"label": _("Manufacturing"), "items": ["Work Order"]},
{"label": _("Internal Transfer"), "items": ["Sales Order"]},
+ {"label": _("Accounting Dimensions"), "items": ["Project", "Cost Center"]},
],
}
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index db9322f..914a9f3 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -74,7 +74,7 @@
"fieldname": "reference_type",
"fieldtype": "Select",
"label": "Reference Type",
- "options": "\nPurchase Receipt\nPurchase Invoice\nDelivery Note\nSales Invoice\nStock Entry\nJob Card",
+ "options": "\nPurchase Receipt\nPurchase Invoice\nSubcontracting Receipt\nDelivery Note\nSales Invoice\nStock Entry\nJob Card",
"reqd": 1
},
{
@@ -245,7 +245,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2022-10-04 22:00:13.995221",
+ "modified": "2023-08-23 11:56:50.282878",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 248b705..258a503 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -647,7 +647,7 @@
def update_distinct_item_warehouses(self, dependant_sle):
key = (dependant_sle.item_code, dependant_sle.warehouse)
- val = frappe._dict({"sle": dependant_sle, "dependent_voucher_detail_nos": []})
+ val = frappe._dict({"sle": dependant_sle})
if key not in self.distinct_item_warehouses:
self.distinct_item_warehouses[key] = val
@@ -661,6 +661,8 @@
if getdate(dependant_sle.posting_date) < getdate(existing_sle_posting_date):
val.sle_changed = True
+ dependent_voucher_detail_nos.append(dependant_sle.voucher_detail_no)
+ val.dependent_voucher_detail_nos = dependent_voucher_detail_nos
self.distinct_item_warehouses[key] = val
self.new_items_found = True
elif dependant_sle.voucher_detail_no not in set(dependent_voucher_detail_nos):
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index 94a2589..e374077 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -3,14 +3,91 @@
frappe.provide('erpnext.buying');
-erpnext.landed_cost_taxes_and_charges.setup_triggers("Subcontracting Receipt");
+erpnext.landed_cost_taxes_and_charges.setup_triggers('Subcontracting Receipt');
frappe.ui.form.on('Subcontracting Receipt', {
setup: (frm) => {
frm.ignore_doctypes_on_cancel_all = ['Serial and Batch Bundle'];
frm.get_field('supplied_items').grid.cannot_add_rows = true;
frm.get_field('supplied_items').grid.only_sortable();
+ frm.trigger('set_queries');
+ },
+ refresh: (frm) => {
+ if (frm.doc.docstatus > 0) {
+ frm.add_custom_button(__('Stock Ledger'), () => {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ from_date: frm.doc.posting_date,
+ to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
+ company: frm.doc.company,
+ show_cancelled_entries: frm.doc.docstatus === 2
+ };
+ frappe.set_route('query-report', 'Stock Ledger');
+ }, __('View'));
+
+ frm.add_custom_button(__('Accounting Ledger'), () => {
+ frappe.route_options = {
+ voucher_no: frm.doc.name,
+ from_date: frm.doc.posting_date,
+ to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
+ company: frm.doc.company,
+ group_by: 'Group by Voucher (Consolidated)',
+ show_cancelled_entries: frm.doc.docstatus === 2
+ };
+ frappe.set_route('query-report', 'General Ledger');
+ }, __('View'));
+ }
+
+ if (!frm.doc.is_return && frm.doc.docstatus === 1 && frm.doc.per_returned < 100) {
+ frm.add_custom_button(__('Subcontract Return'), () => {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return',
+ frm: frm
+ });
+ }, __('Create'));
+ frm.page.set_inner_btn_group_as_primary(__('Create'));
+ }
+
+ if (frm.doc.docstatus === 0) {
+ frm.add_custom_button(__('Subcontracting Order'), () => {
+ if (!frm.doc.supplier) {
+ frappe.throw({
+ title: __('Mandatory'),
+ message: __('Please Select a Supplier')
+ });
+ }
+
+ erpnext.utils.map_current_doc({
+ method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
+ source_doctype: 'Subcontracting Order',
+ target: frm,
+ setters: {
+ supplier: frm.doc.supplier,
+ },
+ get_query_filters: {
+ docstatus: 1,
+ per_received: ['<', 100],
+ company: frm.doc.company
+ }
+ });
+ }, __('Get Items From'));
+
+ frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM');
+ }
+
+ frm.trigger('setup_quality_inspection');
+ },
+
+ set_warehouse: (frm) => {
+ set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse);
+ },
+
+ rejected_warehouse: (frm) => {
+ set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
+ },
+
+ set_queries: (frm) => {
frm.set_query('set_warehouse', () => {
return {
filters: {
@@ -52,38 +129,36 @@
}
}));
- frm.set_query('expense_account', 'items', function () {
- return {
+ frm.set_query('expense_account', 'items', () => ({
query: 'erpnext.controllers.queries.get_expense_account',
filters: { 'company': frm.doc.company }
- };
- });
+ }));
- frm.set_query('batch_no', 'items', function(doc, cdt, cdn) {
+ frm.set_query('batch_no', 'items', (doc, cdt, cdn) => {
var row = locals[cdt][cdn];
return {
filters: {
item: row.item_code
}
- }
+ };
});
- frm.set_query('batch_no', 'supplied_items', function(doc, cdt, cdn) {
+ frm.set_query('batch_no', 'supplied_items', (doc, cdt, cdn) => {
var row = locals[cdt][cdn];
return {
filters: {
item: row.rm_item_code
}
- }
+ };
});
- frm.set_query("serial_and_batch_bundle", "supplied_items", (doc, cdt, cdn) => {
+ frm.set_query('serial_and_batch_bundle', 'supplied_items', (doc, cdt, cdn) => {
let row = locals[cdt][cdn];
return {
filters: {
'item_code': row.rm_item_code,
'voucher_type': doc.doctype,
- 'voucher_no': ["in", [doc.name, ""]],
+ 'voucher_no': ['in', [doc.name, '']],
'is_cancelled': 0,
}
}
@@ -101,7 +176,7 @@
let batch_no_field = frm.get_docfield('items', 'batch_no');
if (batch_no_field) {
- batch_no_field.get_route_options_for_new_doc = function(row) {
+ batch_no_field.get_route_options_for_new_doc = (row) => {
return {
'item': row.doc.item_code
}
@@ -109,85 +184,20 @@
}
},
- refresh: (frm) => {
- if (frm.doc.docstatus > 0) {
- frm.add_custom_button(__('Stock Ledger'), function () {
- frappe.route_options = {
- voucher_no: frm.doc.name,
- from_date: frm.doc.posting_date,
- to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
- company: frm.doc.company,
- show_cancelled_entries: frm.doc.docstatus === 2
- };
- frappe.set_route('query-report', 'Stock Ledger');
- }, __('View'));
-
- frm.add_custom_button(__('Accounting Ledger'), function () {
- frappe.route_options = {
- voucher_no: frm.doc.name,
- from_date: frm.doc.posting_date,
- to_date: moment(frm.doc.modified).format('YYYY-MM-DD'),
- company: frm.doc.company,
- group_by: 'Group by Voucher (Consolidated)',
- show_cancelled_entries: frm.doc.docstatus === 2
- };
- frappe.set_route('query-report', 'General Ledger');
- }, __('View'));
+ setup_quality_inspection: (frm) => {
+ if (!frm.is_new() && frm.doc.docstatus === 0 && !frm.doc.is_return) {
+ let transaction_controller = new erpnext.TransactionController({ frm: frm });
+ transaction_controller.setup_quality_inspection();
}
-
- if (!frm.doc.is_return && frm.doc.docstatus == 1 && frm.doc.per_returned < 100) {
- frm.add_custom_button(__('Subcontract Return'), function () {
- frappe.model.open_mapped_doc({
- method: 'erpnext.subcontracting.doctype.subcontracting_receipt.subcontracting_receipt.make_subcontract_return',
- frm: frm
- });
- }, __('Create'));
- frm.page.set_inner_btn_group_as_primary(__('Create'));
- }
-
- if (frm.doc.docstatus == 0) {
- frm.add_custom_button(__('Subcontracting Order'), function () {
- if (!frm.doc.supplier) {
- frappe.throw({
- title: __('Mandatory'),
- message: __('Please Select a Supplier')
- });
- }
-
- erpnext.utils.map_current_doc({
- method: 'erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order.make_subcontracting_receipt',
- source_doctype: 'Subcontracting Order',
- target: frm,
- setters: {
- supplier: frm.doc.supplier,
- },
- get_query_filters: {
- docstatus: 1,
- per_received: ['<', 100],
- company: frm.doc.company
- }
- });
- }, __('Get Items From'));
-
- frm.fields_dict.supplied_items.grid.update_docfield_property('consumed_qty', 'read_only', frm.doc.__onload && frm.doc.__onload.backflush_based_on === 'BOM');
- }
- },
-
- set_warehouse: (frm) => {
- set_warehouse_in_children(frm.doc.items, 'warehouse', frm.doc.set_warehouse);
- },
-
- rejected_warehouse: (frm) => {
- set_warehouse_in_children(frm.doc.items, 'rejected_warehouse', frm.doc.rejected_warehouse);
},
});
frappe.ui.form.on('Landed Cost Taxes and Charges', {
- amount: function (frm, cdt, cdn) {
+ amount: (frm, cdt, cdn) => {
frm.events.set_base_amount(frm, cdt, cdn);
},
- expense_account: function (frm, cdt, cdn) {
+ expense_account: (frm, cdt, cdn) => {
frm.events.set_account_currency(frm, cdt, cdn);
}
});
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index 60746d9..afe1b60 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -81,6 +81,9 @@
self.validate_posting_time()
self.validate_rejected_warehouse()
+ if not self.get("is_return"):
+ self.validate_inspection()
+
if getdate(self.posting_date) > getdate(nowdate()):
frappe.throw(_("Posting Date cannot be future date"))
@@ -270,17 +273,24 @@
status = "Draft"
elif self.docstatus == 1:
status = "Completed"
+
if self.is_return:
status = "Return"
- return_against = frappe.get_doc("Subcontracting Receipt", self.return_against)
- return_against.run_method("update_status")
elif self.per_returned == 100:
status = "Return Issued"
+
elif self.docstatus == 2:
status = "Cancelled"
+ if self.is_return:
+ frappe.get_doc("Subcontracting Receipt", self.return_against).update_status(
+ update_modified=update_modified
+ )
+
if status:
- frappe.db.set_value("Subcontracting Receipt", self.name, "status", status, update_modified)
+ frappe.db.set_value(
+ "Subcontracting Receipt", self.name, "status", status, update_modified=update_modified
+ )
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index 887cba5..a170527 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -567,6 +567,64 @@
self.assertEqual(rm_item.rate, 100)
self.assertEqual(rm_item.amount, rm_item.consumed_qty * rm_item.rate)
+ def test_quality_inspection_for_subcontracting_receipt(self):
+ from erpnext.stock.doctype.quality_inspection.test_quality_inspection import (
+ create_quality_inspection,
+ )
+
+ set_backflush_based_on("BOM")
+ fg_item = "Subcontracted Item SA1"
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 5,
+ "rate": 100,
+ "fg_item": fg_item,
+ "fg_item_qty": 5,
+ },
+ ]
+ sco = get_subcontracting_order(service_items=service_items)
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+ scr1 = make_subcontracting_receipt(sco.name)
+ scr1.save()
+
+ # Enable `Inspection Required before Purchase` in Item Master
+ frappe.db.set_value("Item", fg_item, "inspection_required_before_purchase", 1)
+
+ # ValidationError should be raised as Quality Inspection is not created/linked
+ self.assertRaises(frappe.ValidationError, scr1.submit)
+
+ qa = create_quality_inspection(
+ reference_type="Subcontracting Receipt",
+ reference_name=scr1.name,
+ inspection_type="Incoming",
+ item_code=fg_item,
+ )
+ scr1.reload()
+ self.assertEqual(scr1.items[0].quality_inspection, qa.name)
+
+ # SCR should be submitted successfully as Quality Inspection is set
+ scr1.submit()
+ qa.cancel()
+ scr1.reload()
+ scr1.cancel()
+
+ scr2 = make_subcontracting_receipt(sco.name)
+ scr2.save()
+
+ # Disable `Inspection Required before Purchase` in Item Master
+ frappe.db.set_value("Item", fg_item, "inspection_required_before_purchase", 0)
+
+ # ValidationError should not be raised as `Inspection Required before Purchase` is disabled
+ scr2.submit()
+
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)
diff --git a/erpnext/translations/fr.csv b/erpnext/translations/fr.csv
index 801604a..3779526 100644
--- a/erpnext/translations/fr.csv
+++ b/erpnext/translations/fr.csv
@@ -3279,7 +3279,7 @@
Quality Feedback Template,Modèle de commentaires sur la qualité,
Rules for applying different promotional schemes.,Règles d'application de différents programmes promotionnels.,
Show {0},Montrer {0},
-"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les séries de nommage {0}",
+"Special Characters except '-', '#', '.', '/', '{{' and '}}' not allowed in naming series {0}","Caractères spéciaux sauf "-", "#", ".", "/", "{{" Et "}}" non autorisés dans les masques de numérotation {0}",
Target Details,Détails de la cible,
{0} already has a Parent Procedure {1}.,{0} a déjà une procédure parent {1}.,
API,API,
@@ -3292,7 +3292,7 @@
Invalid URL,URL invalide,
Landscape,Paysage,
Last Sync On,Dernière synchronisation le,
-Naming Series,Nom de série,
+Naming Series,Masque de numérotation,
No data to export,Aucune donnée à exporter,
Portrait,Portrait,
Print Heading,Imprimer Titre,
@@ -3962,7 +3962,7 @@
Draft,Brouillon,"docstatus,=,0"
Cancelled,Annulé,"docstatus,=,2"
Please setup Instructor Naming System in Education > Education Settings,Veuillez configurer le système de dénomination de l'instructeur dans Éducation> Paramètres de l'éducation,
-Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir la série de noms pour {0} via Configuration> Paramètres> Série de noms,
+Please set Naming Series for {0} via Setup > Settings > Naming Series,Veuillez définir le masque de numérotation pour {0} via Configuration> Paramètres> Série de noms,
UOM Conversion factor ({0} -> {1}) not found for item: {2},Facteur de conversion UdM ({0} -> {1}) introuvable pour l'article: {2},
Item Code > Item Group > Brand,Code article> Groupe d'articles> Marque,
Customer > Customer Group > Territory,Client> Groupe de clients> Territoire,
@@ -3973,7 +3973,7 @@
"Outward taxable supplies(other than zero rated, nil rated and exempted)","Fournitures taxables sortantes (autres que détaxées, nulles et exonérées)",
"To allow different rates, disable the {0} checkbox in {1}.","Pour autoriser différents tarifs, désactivez la {0} case à cocher dans {1}.",
Asset{} {assets_link} created for {},Élément {} {assets_link} créé pour {},
-Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: la série de noms d'éléments est obligatoire pour la création automatique de l'élément {},
+Row {}: Asset Naming Series is mandatory for the auto creation for item {},Ligne {}: Le masque de numérotation d'éléments est obligatoire pour la création automatique de l'élément {},
Assets not created for {0}. You will have to create asset manually.,Éléments non créés pour {0}. Vous devrez créer un actif manuellement.,
{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.,{0} {1} a des écritures comptables dans la devise {2} pour l'entreprise {3}. Veuillez sélectionner un compte à recevoir ou à payer avec la devise {2}.,
Invalid Account,Compte invalide,
@@ -3997,7 +3997,7 @@
Path,Chemin,
Components,Composants,
Verified By,Vérifié Par,
-Invalid naming series (. missing) for {0},Série de noms non valide (. Manquante) pour {0},
+Invalid naming series (. missing) for {0},Masque de numérotation non valide (. Manquante) pour {0},
Filter Based On,Filtre basé sur,
Reqd by date,Reqd par date,
Manufacturer Part Number <b>{0}</b> is invalid,Le numéro de <b>pièce du</b> fabricant <b>{0}</b> n'est pas valide,
@@ -5587,7 +5587,7 @@
Minimum Age,Âge Minimum,
Maximum Age,Âge Maximum,
Application Fee,Frais de Dossier,
-Naming Series (for Student Applicant),Nom de série (pour un candidat étudiant),
+Naming Series (for Student Applicant),Masque de numérotation (pour un candidat étudiant),
LMS Only,LMS seulement,
EDU-APP-.YYYY.-,EDU-APP-YYYY.-,
Application Date,Date de la Candidature,
@@ -6074,7 +6074,7 @@
Hotel Room Reservation Item,Article de réservation de la chambre d'hôtel,
Hotel Settings,Paramètres d'Hotel,
Default Taxes and Charges,Taxes et frais par défaut,
-Default Invoice Naming Series,Numéro de série par défaut pour les factures,
+Default Invoice Naming Series,Masque de numérotation par défaut pour les factures,
HR,RH,
Date on which this component is applied,Date à laquelle ce composant est appliqué,
Salary Slip,Fiche de Paie,
@@ -7136,7 +7136,7 @@
Maintain Stock,Maintenir Stock,
Standard Selling Rate,Prix de Vente Standard,
Auto Create Assets on Purchase,Création automatique d'actifs à l'achat,
-Asset Naming Series,Nom de série de l'actif,
+Asset Naming Series,Masque de numérotation de l'actif,
Over Delivery/Receipt Allowance (%),Surlivrance / indemnité de réception (%),
Barcodes,Codes-barres,
Shelf Life In Days,Durée de conservation en jours,
@@ -7155,7 +7155,7 @@
Has Batch No,A un Numéro de Lot,
Automatically Create New Batch,Créer un Nouveau Lot Automatiquement,
Batch Number Series,Série de numéros de lots,
-"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si la série est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec cette série. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe de la série dans les paramètres de stock.",
+"Example: ABCD.#####. If series is set and Batch No is not mentioned in transactions, then automatic batch number will be created based on this series. If you always want to explicitly mention Batch No for this item, leave this blank. Note: this setting will take priority over the Naming Series Prefix in Stock Settings.","Exemple: ABCD. #####. Si le masque est définie et que le numéro de lot n'est pas mentionné dans les transactions, un numéro de lot sera automatiquement créé en avec ce masque. Si vous préferez mentionner explicitement et systématiquement le numéro de lot pour cet article, laissez ce champ vide. Remarque: ce paramètre aura la priorité sur le préfixe du masque dans les paramètres de stock.",
Has Expiry Date,A une date d'expiration,
Retain Sample,Conserver l'échantillon,
Max Sample Quantity,Quantité maximum d'échantillon,
@@ -7455,8 +7455,8 @@
Freeze Stock Entries,Geler les Entrées de Stocks,
Stock Frozen Upto,Stock Gelé Jusqu'au,
Batch Identification,Identification par lots,
-Use Naming Series,Utiliser la série de noms,
-Naming Series Prefix,Préfix du nom de série,
+Use Naming Series,Utiliser le masque de numérotation,
+Naming Series Prefix,Préfix du masque de numérotation,
UOM Category,Catégorie d'unité de mesure (UdM),
UOM Conversion Detail,Détails de Conversion de l'UdM,
Variant Field,Champ de Variante,
@@ -7914,7 +7914,7 @@
Purchase Details,Détails d'achat,
Depreciation Posting Date,Date comptable de l'amortissement,
"By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ","Par défaut, le nom du fournisseur est défini selon le nom du fournisseur saisi. Si vous souhaitez que les fournisseurs soient nommés par un",
- choose the 'Naming Series' option.,choisissez l'option 'Naming Series'.,
+ choose the 'Naming Series' option.,choisissez l'option 'Masque de numérotation'.,
Configure the default Price List when creating a new Purchase transaction. Item prices will be fetched from this Price List.,Configurez la liste de prix par défaut lors de la création d'une nouvelle transaction d'achat. Les prix des articles seront extraits de cette liste de prix.,
"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice or Receipt without creating a Purchase Order first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Order' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat ou un reçu sans créer d'abord une Commande d'Achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case «Autoriser la création de facture d'achat sans commmande d'achat» dans la fiche fournisseur.",
"If this option is configured 'Yes', ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first. This configuration can be overridden for a particular supplier by enabling the 'Allow Purchase Invoice Creation Without Purchase Receipt' checkbox in the Supplier master.","Si cette option est configurée «Oui», ERPNext vous empêchera de créer une facture d'achat sans créer d'abord un reçu d'achat. Cette configuration peut être remplacée pour un fournisseur particulier en cochant la case "Autoriser la création de facture d'achat sans reçu d'achat" dans la fiche fournisseur.",
@@ -8871,7 +8871,7 @@
Update Existing Price List Rate,Mise a jour automatique du prix dans les listes de prix,
Show Barcode Field in Stock Transactions,Afficher le champ Code Barre dans les transactions de stock,
Convert Item Description to Clean HTML in Transactions,Convertir les descriptions d'articles en HTML valide lors des transactions,
-Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries,
+Have Default Naming Series for Batch ID?,Masque de numérotation par défaut pour les Lots ou Séries,
"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités"
Allowed Items,Articles autorisés,
Party Specific Item,Restriction d'article disponible,
@@ -8925,3 +8925,15 @@
Enable Recommendations,Activer les recommendations,
Item Search Settings,Paramétrage de la recherche d'article,
Purchase demande,Demande de materiel,
+Internal Customer,Client interne
+Internal Supplier,Fournisseur interne
+Contact & Address,Contact et Adresse
+Primary Address and Contact,Adresse et contact principal
+Supplier Primary Contact,Contact fournisseur principal
+Supplier Primary Address,Adresse fournisseur principal
+From Opportunity,Depuis l'opportunité
+Default Receivable Accounts,Compte de débit par défaut
+Receivable Accounts,Compte de débit
+Mention if a non-standard receivable account,Veuillez mentionner s'il s'agit d'un compte débiteur non standard
+Allow Purchase,Autoriser à l'achat
+Inventory Settings,Paramétrage de l'inventaire