Merge branch 'develop' into develop-ritvik-ignore-perm-tree-doctype
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2c9a60c..30be903 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -40,6 +40,7 @@
- id: flake8
additional_dependencies: [
'flake8-bugbear',
+ 'flake8-tuple',
]
args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$"
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/account/chart_of_accounts/verified/mx_plan_de_cuentas.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/mx_plan_de_cuentas.json
index e98c2d6..858f05c 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/mx_plan_de_cuentas.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/mx_plan_de_cuentas.json
@@ -109,8 +109,7 @@
}
},
"INVENTARIOS": {
- "account_type": "Stock",
- "is_group": 1
+ "account_type": "Stock"
}
},
"ACTIVO LARGO PLAZO": {
@@ -398,10 +397,18 @@
"INGRESOS POR SERVICIOS 1": {}
},
"VENTAS": {
- "VENTAS EXPORTACION": {},
- "VENTAS INMUEBLES": {},
- "VENTAS NACIONALES": {},
- "VENTAS NACIONALES AL DETAL": {}
+ "VENTAS EXPORTACION": {
+ "account_type": "Income Account"
+ },
+ "VENTAS INMUEBLES": {
+ "account_type": "Income Account"
+ },
+ "VENTAS NACIONALES": {
+ "account_type": "Income Account"
+ },
+ "VENTAS NACIONALES AL DETAL": {
+ "account_type": "Income Account"
+ }
}
}
},
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
index ec55e60..e520872 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/test_exchange_rate_revaluation.py
@@ -3,6 +3,296 @@
import unittest
+import frappe
+from frappe import qb
+from frappe.tests.utils import FrappeTestCase, change_settings
+from frappe.utils import add_days, flt, today
-class TestExchangeRateRevaluation(unittest.TestCase):
- pass
+from erpnext import get_default_cost_center
+from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.party import get_party_account
+from erpnext.accounts.test.accounts_mixin import AccountsTestMixin
+from erpnext.stock.doctype.item.test_item import create_item
+
+
+class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
+ def setUp(self):
+ self.create_company()
+ self.create_usd_receivable_account()
+ self.create_item()
+ self.create_customer()
+ self.clear_old_entries()
+ self.set_system_and_company_settings()
+
+ def tearDown(self):
+ frappe.db.rollback()
+
+ def set_system_and_company_settings(self):
+ # set number and currency precision
+ system_settings = frappe.get_doc("System Settings")
+ system_settings.float_precision = 2
+ system_settings.currency_precision = 2
+ system_settings.save()
+
+ # Using Exchange Gain/Loss account for unrealized as well.
+ company_doc = frappe.get_doc("Company", self.company)
+ company_doc.unrealized_exchange_gain_loss_account = company_doc.exchange_gain_loss_account
+ company_doc.save()
+
+ @change_settings(
+ "Accounts Settings",
+ {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+ )
+ def test_01_revaluation_of_forex_balance(self):
+ """
+ Test Forex account balance and Journal creation post Revaluation
+ """
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debtors_usd,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ price_list_rate=100,
+ do_not_submit=1,
+ )
+ si.currency = "USD"
+ si.conversion_rate = 80
+ si.save().submit()
+
+ err = frappe.new_doc("Exchange Rate Revaluation")
+ err.company = self.company
+ err.posting_date = today()
+ accounts = err.get_accounts_data()
+ err.extend("accounts", accounts)
+ row = err.accounts[0]
+ row.new_exchange_rate = 85
+ row.new_balance_in_base_currency = flt(
+ row.new_exchange_rate * flt(row.balance_in_account_currency)
+ )
+ row.gain_loss = row.new_balance_in_base_currency - flt(row.balance_in_base_currency)
+ err.set_total_gain_loss()
+ err = err.save().submit()
+
+ # Create JV for ERR
+ err_journals = err.make_jv_entries()
+ je = frappe.get_doc("Journal Entry", err_journals.get("revaluation_jv"))
+ je = je.submit()
+
+ je.reload()
+ self.assertEqual(je.voucher_type, "Exchange Rate Revaluation")
+ self.assertEqual(je.total_debit, 8500.0)
+ self.assertEqual(je.total_credit, 8500.0)
+
+ acc_balance = frappe.db.get_all(
+ "GL Entry",
+ filters={"account": self.debtors_usd, "is_cancelled": 0},
+ fields=["sum(debit)-sum(credit) as balance"],
+ )[0]
+ self.assertEqual(acc_balance.balance, 8500.0)
+
+ @change_settings(
+ "Accounts Settings",
+ {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+ )
+ def test_02_accounts_only_with_base_currency_balance(self):
+ """
+ Test Revaluation on Forex account with balance only in base currency
+ """
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debtors_usd,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ price_list_rate=100,
+ do_not_submit=1,
+ )
+ si.currency = "USD"
+ si.conversion_rate = 80
+ si.save().submit()
+
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.source_exchange_rate = 85
+ pe.received_amount = 8500
+ pe.save().submit()
+
+ # Cancel the auto created gain/loss JE to simulate balance only in base currency
+ je = frappe.db.get_all(
+ "Journal Entry Account", filters={"reference_name": si.name}, pluck="parent"
+ )[0]
+ frappe.get_doc("Journal Entry", je).cancel()
+
+ err = frappe.new_doc("Exchange Rate Revaluation")
+ err.company = self.company
+ err.posting_date = today()
+ err.fetch_and_calculate_accounts_data()
+ err = err.save().submit()
+
+ # Create JV for ERR
+ self.assertTrue(err.check_journal_entry_condition())
+ err_journals = err.make_jv_entries()
+ je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
+ je = je.submit()
+
+ je.reload()
+ self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
+ self.assertEqual(len(je.accounts), 2)
+ # Only base currency fields will be posted to
+ for acc in je.accounts:
+ self.assertEqual(acc.debit_in_account_currency, 0)
+ self.assertEqual(acc.credit_in_account_currency, 0)
+
+ self.assertEqual(je.total_debit, 500.0)
+ self.assertEqual(je.total_credit, 500.0)
+
+ acc_balance = frappe.db.get_all(
+ "GL Entry",
+ filters={"account": self.debtors_usd, "is_cancelled": 0},
+ fields=[
+ "sum(debit)-sum(credit) as balance",
+ "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
+ ],
+ )[0]
+ # account shouldn't have balance in base and account currency
+ self.assertEqual(acc_balance.balance, 0.0)
+ self.assertEqual(acc_balance.balance_in_account_currency, 0.0)
+
+ @change_settings(
+ "Accounts Settings",
+ {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+ )
+ def test_03_accounts_only_with_account_currency_balance(self):
+ """
+ Test Revaluation on Forex account with balance only in account currency
+ """
+ precision = frappe.db.get_single_value("System Settings", "currency_precision")
+
+ # posting on previous date to make sure that ERR picks up the Payment entry's exchange
+ # rate while calculating gain/loss for account currency balance
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debtors_usd,
+ posting_date=add_days(today(), -1),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ price_list_rate=100,
+ do_not_submit=1,
+ )
+ si.currency = "USD"
+ si.conversion_rate = 80
+ si.save().submit()
+
+ pe = get_payment_entry(si.doctype, si.name)
+ pe.paid_amount = 95
+ pe.source_exchange_rate = 84.211
+ pe.received_amount = 8000
+ pe.references = []
+ pe.save().submit()
+
+ acc_balance = frappe.db.get_all(
+ "GL Entry",
+ filters={"account": self.debtors_usd, "is_cancelled": 0},
+ fields=[
+ "sum(debit)-sum(credit) as balance",
+ "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
+ ],
+ )[0]
+ # account should have balance only in account currency
+ self.assertEqual(flt(acc_balance.balance, precision), 0.0)
+ self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 5.0) # in USD
+
+ err = frappe.new_doc("Exchange Rate Revaluation")
+ err.company = self.company
+ err.posting_date = today()
+ err.fetch_and_calculate_accounts_data()
+ err.set_total_gain_loss()
+ err = err.save().submit()
+
+ # Create JV for ERR
+ self.assertTrue(err.check_journal_entry_condition())
+ err_journals = err.make_jv_entries()
+ je = frappe.get_doc("Journal Entry", err_journals.get("zero_balance_jv"))
+ je = je.submit()
+
+ je.reload()
+ self.assertEqual(je.voucher_type, "Exchange Gain Or Loss")
+ self.assertEqual(len(je.accounts), 2)
+ # Only account currency fields will be posted to
+ for acc in je.accounts:
+ self.assertEqual(flt(acc.debit, precision), 0.0)
+ self.assertEqual(flt(acc.credit, precision), 0.0)
+
+ row = [x for x in je.accounts if x.account == self.debtors_usd][0]
+ self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
+ row = [x for x in je.accounts if x.account != self.debtors_usd][0]
+ self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
+
+ # total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
+ self.assertEqual(flt(je.total_debit, precision), 0.0)
+ self.assertEqual(flt(je.total_credit, precision), 0.0)
+
+ acc_balance = frappe.db.get_all(
+ "GL Entry",
+ filters={"account": self.debtors_usd, "is_cancelled": 0},
+ fields=[
+ "sum(debit)-sum(credit) as balance",
+ "sum(debit_in_account_currency)-sum(credit_in_account_currency) as balance_in_account_currency",
+ ],
+ )[0]
+ # account shouldn't have balance in base and account currency post revaluation
+ self.assertEqual(flt(acc_balance.balance, precision), 0.0)
+ self.assertEqual(flt(acc_balance.balance_in_account_currency, precision), 0.0)
+
+ @change_settings(
+ "Accounts Settings",
+ {"allow_multi_currency_invoices_against_single_party_account": 1, "allow_stale": 0},
+ )
+ def test_04_get_account_details_function(self):
+ si = create_sales_invoice(
+ item=self.item,
+ company=self.company,
+ customer=self.customer,
+ debit_to=self.debtors_usd,
+ posting_date=today(),
+ parent_cost_center=self.cost_center,
+ cost_center=self.cost_center,
+ rate=100,
+ price_list_rate=100,
+ do_not_submit=1,
+ )
+ si.currency = "USD"
+ si.conversion_rate = 80
+ si.save().submit()
+
+ from erpnext.accounts.doctype.exchange_rate_revaluation.exchange_rate_revaluation import (
+ get_account_details,
+ )
+
+ account_details = get_account_details(
+ self.company, si.posting_date, self.debtors_usd, "Customer", self.customer, 0.05
+ )
+ # not checking for new exchange rate and balances as it is dependent on live exchange rates
+ expected_data = {
+ "account_currency": "USD",
+ "balance_in_base_currency": 8000.0,
+ "balance_in_account_currency": 100.0,
+ "current_exchange_rate": 80.0,
+ "zero_balance": False,
+ "new_balance_in_account_currency": 100.0,
+ }
+
+ for key, val in expected_data.items():
+ self.assertEqual(expected_data.get(key), account_details.get(key))
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index ac31e8a..9ed3d32 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -999,14 +999,14 @@
if self.payment_type == "Internal Transfer":
remarks = [
_("Amount {0} {1} transferred from {2} to {3}").format(
- self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to
+ _(self.paid_from_account_currency), self.paid_amount, self.paid_from, self.paid_to
)
]
else:
remarks = [
_("Amount {0} {1} {2} {3}").format(
- self.party_account_currency,
+ _(self.party_account_currency),
self.paid_amount if self.payment_type == "Receive" else self.received_amount,
_("received from") if self.payment_type == "Receive" else _("to"),
self.party,
@@ -1023,14 +1023,14 @@
if d.allocated_amount:
remarks.append(
_("Amount {0} {1} against {2} {3}").format(
- self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name
+ _(self.party_account_currency), d.allocated_amount, d.reference_doctype, d.reference_name
)
)
for d in self.get("deductions"):
if d.amount:
remarks.append(
- _("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account)
+ _("Amount {0} {1} deducted against {2}").format(_(self.company_currency), d.amount, d.account)
)
self.set("remarks", "\n".join(remarks))
@@ -1993,10 +1993,15 @@
if not total_amount:
if party_account_currency == company_currency:
# for handling cases that don't have multi-currency (base field)
- total_amount = ref_doc.get("base_grand_total") or ref_doc.get("grand_total")
+ total_amount = (
+ ref_doc.get("base_rounded_total")
+ or ref_doc.get("rounded_total")
+ or ref_doc.get("base_grand_total")
+ or ref_doc.get("grand_total")
+ )
exchange_rate = 1
else:
- total_amount = ref_doc.get("grand_total")
+ total_amount = ref_doc.get("rounded_total") or ref_doc.get("grand_total")
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc.
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index c8bf664..edfec41 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -1244,6 +1244,24 @@
template.allocate_payment_based_on_payment_terms = 1
template.save()
+ def test_allocation_validation_for_sales_order(self):
+ so = make_sales_order(do_not_save=True)
+ so.items[0].rate = 99.55
+ so.save().submit()
+ self.assertGreater(so.rounded_total, 0.0)
+ pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
+ pe.paid_from = "Debtors - _TC"
+ pe.paid_amount = 45.55
+ pe.references[0].allocated_amount = 45.55
+ pe.save().submit()
+ pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
+ pe.paid_from = "Debtors - _TC"
+ # No validation error should be thrown here.
+ pe.save().submit()
+
+ so.reload()
+ self.assertEqual(so.advance_paid, so.rounded_total)
+
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 9f1224d..be19bca 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -759,21 +759,22 @@
# Amount added through landed-cost-voucher
if landed_cost_entries:
- for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
- gl_entries.append(
- self.get_gl_dict(
- {
- "account": account,
- "against": item.expense_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(amount["base_amount"]),
- "credit_in_account_currency": flt(amount["amount"]),
- "project": item.project or self.project,
- },
- item=item,
+ if (item.item_code, item.name) in landed_cost_entries:
+ for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": account,
+ "against": item.expense_account,
+ "cost_center": item.cost_center,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "credit": flt(amount["base_amount"]),
+ "credit_in_account_currency": flt(amount["amount"]),
+ "project": item.project or self.project,
+ },
+ item=item,
+ )
)
- )
# sub-contracting warehouse
if flt(item.rm_supp_cost):
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/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/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/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py
index c7b7e2f..ca8b9f0 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -10,8 +10,8 @@
from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.utils import (
+ apply_common_conditions,
get_advance_taxes_and_charges,
- get_conditions,
get_journal_entries,
get_opening_row,
get_party_details,
@@ -378,11 +378,8 @@
def get_invoices(filters, additional_query_columns):
pi = frappe.qb.DocType("Purchase Invoice")
- invoice_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(pi)
- .inner_join(invoice_item)
- .on(pi.name == invoice_item.parent)
.select(
ConstantColumn("Purchase Invoice").as_("doctype"),
pi.name,
@@ -402,23 +399,39 @@
.where((pi.docstatus == 1))
.orderby(pi.posting_date, pi.name, order=Order.desc)
)
+
if additional_query_columns:
for col in additional_query_columns:
query = query.select(col)
+
if filters.get("supplier"):
query = query.where(pi.supplier == filters.supplier)
- query = get_conditions(
+
+ query = get_conditions(filters, query, "Purchase Invoice")
+
+ query = apply_common_conditions(
filters, query, doctype="Purchase Invoice", child_doctype="Purchase Invoice Item"
)
+
if filters.get("include_payments"):
party_account = get_party_account(
"Supplier", filters.get("supplier"), filters.get("company"), include_advance=True
)
query = query.where(pi.credit_to.isin(party_account))
+
invoices = query.run(as_dict=True)
return invoices
+def get_conditions(filters, query, doctype):
+ parent_doc = frappe.qb.DocType(doctype)
+
+ if filters.get("mode_of_payment"):
+ query = query.where(parent_doc.mode_of_payment == filters.mode_of_payment)
+
+ return query
+
+
def get_payments(filters):
args = frappe._dict(
account="credit_to",
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index 35d8d16..d3fc373 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -11,8 +11,8 @@
from erpnext.accounts.party import get_party_account
from erpnext.accounts.report.utils import (
+ apply_common_conditions,
get_advance_taxes_and_charges,
- get_conditions,
get_journal_entries,
get_opening_row,
get_party_details,
@@ -415,14 +415,8 @@
def get_invoices(filters, additional_query_columns):
si = frappe.qb.DocType("Sales Invoice")
- invoice_item = frappe.qb.DocType("Sales Invoice Item")
- invoice_payment = frappe.qb.DocType("Sales Invoice Payment")
query = (
frappe.qb.from_(si)
- .inner_join(invoice_item)
- .on(si.name == invoice_item.parent)
- .left_join(invoice_payment)
- .on(si.name == invoice_payment.parent)
.select(
ConstantColumn("Sales Invoice").as_("doctype"),
si.name,
@@ -447,18 +441,36 @@
.where((si.docstatus == 1))
.orderby(si.posting_date, si.name, order=Order.desc)
)
+
if additional_query_columns:
for col in additional_query_columns:
query = query.select(col)
+
if filters.get("customer"):
query = query.where(si.customer == filters.customer)
- query = get_conditions(
+
+ query = get_conditions(filters, query, "Sales Invoice")
+ query = apply_common_conditions(
filters, query, doctype="Sales Invoice", child_doctype="Sales Invoice Item"
)
+
invoices = query.run(as_dict=True)
return invoices
+def get_conditions(filters, query, doctype):
+ parent_doc = frappe.qb.DocType(doctype)
+ if filters.get("owner"):
+ query = query.where(parent_doc.owner == filters.owner)
+
+ if filters.get("mode_of_payment"):
+ payment_doc = frappe.qb.DocType("Sales Invoice Payment")
+ query = query.inner_join(payment_doc).on(parent_doc.name == payment_doc.parent)
+ query = query.where(payment_doc.mode_of_payment == filters.mode_of_payment).distinct()
+
+ return query
+
+
def get_payments(filters):
args = frappe._dict(
account="debit_to",
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/report/utils.py b/erpnext/accounts/report/utils.py
index 0753fff..9f96449 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -256,7 +256,8 @@
)
.orderby(je.posting_date, je.name, order=Order.desc)
)
- query = get_conditions(filters, query, doctype="Journal Entry", payments=True)
+ query = apply_common_conditions(filters, query, doctype="Journal Entry", payments=True)
+
journal_entries = query.run(as_dict=True)
return journal_entries
@@ -284,28 +285,17 @@
)
.orderby(pe.posting_date, pe.name, order=Order.desc)
)
- query = get_conditions(filters, query, doctype="Payment Entry", payments=True)
+ query = apply_common_conditions(filters, query, doctype="Payment Entry", payments=True)
payment_entries = query.run(as_dict=True)
return payment_entries
-def get_conditions(filters, query, doctype, child_doctype=None, payments=False):
+def apply_common_conditions(filters, query, doctype, child_doctype=None, payments=False):
parent_doc = frappe.qb.DocType(doctype)
if child_doctype:
child_doc = frappe.qb.DocType(child_doctype)
- if parent_doc.get_table_name() == "tabSales Invoice":
- if filters.get("owner"):
- query = query.where(parent_doc.owner == filters.owner)
- if filters.get("mode_of_payment"):
- payment_doc = frappe.qb.DocType("Sales Invoice Payment")
- query = query.where(payment_doc.mode_of_payment == filters.mode_of_payment)
- if not payments:
- if filters.get("brand"):
- query = query.where(child_doc.brand == filters.brand)
- else:
- if filters.get("mode_of_payment"):
- query = query.where(parent_doc.mode_of_payment == filters.mode_of_payment)
+ join_required = False
if filters.get("company"):
query = query.where(parent_doc.company == filters.company)
@@ -320,13 +310,26 @@
else:
if filters.get("cost_center"):
query = query.where(child_doc.cost_center == filters.cost_center)
+ join_required = True
if filters.get("warehouse"):
query = query.where(child_doc.warehouse == filters.warehouse)
+ join_required = True
if filters.get("item_group"):
query = query.where(child_doc.item_group == filters.item_group)
+ join_required = True
+
+ if not payments:
+ if filters.get("brand"):
+ query = query.where(child_doc.brand == filters.brand)
+ join_required = True
+
+ if join_required:
+ query = query.inner_join(child_doc).on(parent_doc.name == child_doc.parent)
+ query = query.distinct()
if parent_doc.get_table_name() != "tabJournal Entry":
query = filter_invoices_based_on_dimensions(filters, query, parent_doc)
+
return query
diff --git a/erpnext/accounts/test/accounts_mixin.py b/erpnext/accounts/test/accounts_mixin.py
index debfffd..bf01362 100644
--- a/erpnext/accounts/test/accounts_mixin.py
+++ b/erpnext/accounts/test/accounts_mixin.py
@@ -60,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
@@ -105,6 +104,28 @@
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",
@@ -113,6 +134,8 @@
"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 9d6d0f9..1aefeaa 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -908,9 +908,9 @@
min_outstanding=None,
max_outstanding=None,
accounting_dimensions=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
+ 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")
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index bf62a8f..383be97 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -156,6 +156,8 @@
def prepare_chart_data(data, filters):
+ if not data:
+ return
labels_values_map = {}
if filters.filter_based_on not in ("Date Range", "Fiscal Year"):
filters_filter_based_on = "Date Range"
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 955ebef..1d50639 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -201,9 +201,9 @@
# apply tax withholding only if checked and applicable
self.set_tax_withholding()
- validate_regional(self)
-
- validate_einvoice_fields(self)
+ with temporary_flag("company", self.company):
+ validate_regional(self)
+ validate_einvoice_fields(self)
if self.doctype != "Material Request" and not self.ignore_pricing_rule:
apply_pricing_rule_on_transaction(self)
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index b396b27..b1ce539 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -436,24 +436,6 @@
# validate rate with ref PR
- def validate_rejected_warehouse(self):
- for item in self.get("items"):
- if flt(item.rejected_qty) and not item.rejected_warehouse:
- if self.rejected_warehouse:
- item.rejected_warehouse = self.rejected_warehouse
-
- if not item.rejected_warehouse:
- frappe.throw(
- _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format(
- item.idx, item.item_code
- )
- )
-
- if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")):
- frappe.throw(
- _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx)
- )
-
# validate accepted and rejected qty
def validate_accepted_rejected_qty(self):
for d in self.get("items"):
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/selling_controller.py b/erpnext/controllers/selling_controller.py
index 6f1a50d..9771f60 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -388,7 +388,7 @@
for d in self.get("items"):
if d.get(ref_fieldname):
status = frappe.db.get_value("Sales Order", d.get(ref_fieldname), "status")
- if status in ("Closed", "On Hold"):
+ if status in ("Closed", "On Hold") and not self.is_return:
frappe.throw(_("Sales Order {0} is {1}").format(d.get(ref_fieldname), status))
def update_reserved_qty(self):
@@ -404,7 +404,9 @@
if so and so_item_rows:
sales_order = frappe.get_doc("Sales Order", so)
- if sales_order.status in ["Closed", "Cancelled"]:
+ if (sales_order.status == "Closed" and not self.is_return) or sales_order.status in [
+ "Cancelled"
+ ]:
frappe.throw(
_("{0} {1} is cancelled or closed").format(_("Sales Order"), so), frappe.InvalidStatusError
)
diff --git a/erpnext/controllers/subcontracting_controller.py b/erpnext/controllers/subcontracting_controller.py
index 6633f4f..d4270a7 100644
--- a/erpnext/controllers/subcontracting_controller.py
+++ b/erpnext/controllers/subcontracting_controller.py
@@ -55,6 +55,23 @@
else:
super(SubcontractingController, self).validate()
+ def validate_rejected_warehouse(self):
+ for item in self.get("items"):
+ if flt(item.rejected_qty) and not item.rejected_warehouse:
+ if self.rejected_warehouse:
+ item.rejected_warehouse = self.rejected_warehouse
+ else:
+ frappe.throw(
+ _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format(
+ item.idx, item.item_code
+ )
+ )
+
+ if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")):
+ frappe.throw(
+ _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx)
+ )
+
def remove_empty_rows(self):
for key in ["service_items", "items", "supplied_items"]:
if self.get(key):
@@ -80,23 +97,27 @@
if not is_stock_item:
frappe.throw(_("Row {0}: Item {1} must be a stock item.").format(item.idx, item.item_name))
- if not is_sub_contracted_item:
- frappe.throw(
- _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
- )
+ if not item.get("is_scrap_item"):
+ if not is_sub_contracted_item:
+ frappe.throw(
+ _("Row {0}: Item {1} must be a subcontracted item.").format(item.idx, item.item_name)
+ )
- if item.bom:
- bom = frappe.get_doc("BOM", item.bom)
- if not bom.is_active:
- frappe.throw(
- _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name)
- )
- if bom.item != item.item_code:
- frappe.throw(
- _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name)
- )
+ if item.bom:
+ is_active, bom_item = frappe.get_value("BOM", item.bom, ["is_active", "item"])
+
+ if not is_active:
+ frappe.throw(
+ _("Row {0}: Please select an active BOM for Item {1}.").format(item.idx, item.item_name)
+ )
+ if bom_item != item.item_code:
+ frappe.throw(
+ _("Row {0}: Please select an valid BOM for Item {1}.").format(item.idx, item.item_name)
+ )
+ else:
+ frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name))
else:
- frappe.throw(_("Row {0}: Please select a BOM for Item {1}.").format(item.idx, item.item_name))
+ item.bom = None
def __get_data_before_save(self):
item_dict = {}
@@ -874,19 +895,24 @@
if self.total_additional_costs:
if self.distribute_additional_costs_based_on == "Amount":
- total_amt = sum(flt(item.amount) for item in self.get("items"))
+ total_amt = sum(
+ flt(item.amount) for item in self.get("items") if not item.get("is_scrap_item")
+ )
for item in self.items:
- item.additional_cost_per_qty = (
- (item.amount * self.total_additional_costs) / total_amt
- ) / item.qty
+ if not item.get("is_scrap_item"):
+ item.additional_cost_per_qty = (
+ (item.amount * self.total_additional_costs) / total_amt
+ ) / item.qty
else:
- total_qty = sum(flt(item.qty) for item in self.get("items"))
+ total_qty = sum(flt(item.qty) for item in self.get("items") if not item.get("is_scrap_item"))
additional_cost_per_qty = self.total_additional_costs / total_qty
for item in self.items:
- item.additional_cost_per_qty = additional_cost_per_qty
+ if not item.get("is_scrap_item"):
+ item.additional_cost_per_qty = additional_cost_per_qty
else:
for item in self.items:
- item.additional_cost_per_qty = 0
+ if not item.get("is_scrap_item"):
+ item.additional_cost_per_qty = 0
@frappe.whitelist()
def get_current_stock(self):
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 0cb8824..dafbd9f 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -516,7 +516,7 @@
"idx": 5,
"image_field": "image",
"links": [],
- "modified": "2023-04-14 18:20:05.044791",
+ "modified": "2023-08-28 22:28:00.104413",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -527,7 +527,7 @@
"permlevel": 1,
"read": 1,
"report": 1,
- "role": "All"
+ "role": "Desk User"
},
{
"create": 1,
@@ -583,4 +583,4 @@
"states": [],
"subject_field": "title",
"title_field": "title"
-}
+}
\ No newline at end of file
diff --git a/erpnext/e_commerce/shopping_cart/cart.py b/erpnext/e_commerce/shopping_cart/cart.py
index 4c82393..85d9a65 100644
--- a/erpnext/e_commerce/shopping_cart/cart.py
+++ b/erpnext/e_commerce/shopping_cart/cart.py
@@ -517,6 +517,8 @@
}
)
+ customer.append("portal_users", {"user": user})
+
if debtors_account:
customer.update({"accounts": [{"company": cart_settings.company, "account": debtors_account}]})
diff --git a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
index 6d977e0..2b2da7b 100644
--- a/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
+++ b/erpnext/erpnext_integrations/connectors/woocommerce_connection.py
@@ -41,7 +41,10 @@
if frappe.flags.woocomm_test_order_data:
order = frappe.flags.woocomm_test_order_data
event = "created"
-
+ # Ignore the test ping issued during WooCommerce webhook configuration
+ # Ref: https://github.com/woocommerce/woocommerce/issues/15642
+ if frappe.request.data.decode("utf-8").startswith("webhook_id="):
+ return "success"
elif frappe.request and frappe.request.data:
verify_request()
try:
@@ -81,7 +84,9 @@
customer.save()
if customer_exists:
- frappe.rename_doc("Customer", old_name, customer_name)
+ # Fixes https://github.com/frappe/erpnext/issues/33708
+ if old_name != customer_name:
+ frappe.rename_doc("Customer", old_name, customer_name)
for address_type in (
"Billing",
"Shipping",
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
index 5c4be6f..510317f 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -1,6 +1,6 @@
{
"charts": [],
- "content": "[{\"id\":\"e88ADOJ7WC\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Integrations</b></span>\",\"col\":12}},{\"id\":\"G0tyx9WOfm\",\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"id\":\"nu4oSjH5Rd\",\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"id\":\"nG8cdkpzoc\",\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"id\":\"4hwuQn6E95\",\"type\":\"card\",\"data\":{\"card_name\":\"Communication Channels\",\"col\":4}},{\"id\":\"sEGAzTJRmq\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}},{\"id\":\"ZC6xu-cLBR\",\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]",
+ "content": "[{\"id\":\"e88ADOJ7WC\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Integrations</b></span>\",\"col\":12}},{\"id\":\"pZEYOOCdB0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Browse Apps\",\"col\":3}},{\"id\":\"St7AHbhVOr\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"nu4oSjH5Rd\",\"type\":\"card\",\"data\":{\"card_name\":\"Authentication\",\"col\":4}},{\"id\":\"G0tyx9WOfm\",\"type\":\"card\",\"data\":{\"card_name\":\"Backup\",\"col\":4}},{\"id\":\"nG8cdkpzoc\",\"type\":\"card\",\"data\":{\"card_name\":\"Google Services\",\"col\":4}},{\"id\":\"4hwuQn6E95\",\"type\":\"card\",\"data\":{\"card_name\":\"Communication Channels\",\"col\":4}},{\"id\":\"sEGAzTJRmq\",\"type\":\"card\",\"data\":{\"card_name\":\"Payments\",\"col\":4}}]",
"creation": "2020-08-20 19:30:48.138801",
"custom_blocks": [],
"docstatus": 0,
@@ -221,27 +221,9 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Settings",
- "link_count": 2,
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Woocommerce Settings",
- "link_count": 0,
- "link_to": "Woocommerce Settings",
- "link_type": "DocType",
- "onboard": 0,
- "type": "Link"
}
],
- "modified": "2023-05-24 14:47:26.984717",
+ "modified": "2023-08-29 15:48:59.010704",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",
@@ -253,6 +235,14 @@
"restrict_to_domain": "",
"roles": [],
"sequence_id": 21.0,
- "shortcuts": [],
+ "shortcuts": [
+ {
+ "color": "Grey",
+ "doc_view": "List",
+ "label": "Browse Apps",
+ "type": "URL",
+ "url": "https://frappecloud.com/marketplace"
+ }
+ ],
"title": "ERPNext Integrations"
}
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 7eaa146..41db6b3 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -198,7 +198,7 @@
]
standard_portal_menu_items = [
- {"title": "Projects", "route": "/project", "reference_doctype": "Project"},
+ {"title": "Projects", "route": "/project", "reference_doctype": "Project", "role": "Customer"},
{
"title": "Request for Quotations",
"route": "/rfq",
@@ -290,6 +290,7 @@
"Delivery Note": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Issue": "erpnext.support.doctype.issue.issue.has_website_permission",
"Timesheet": "erpnext.controllers.website_list_for_contact.has_website_permission",
+ "Project": "erpnext.controllers.website_list_for_contact.has_website_permission",
}
before_tests = "erpnext.setup.utils.before_tests"
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 131f438..34e9423 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -53,7 +53,7 @@
data = sales_order_query(filters={"company": self.company, "sales_orders": sales_orders})
title = _("Production Plan Already Submitted")
- if not data:
+ if not data and sales_orders:
msg = _("No items are available in the sales order {0} for production").format(sales_orders[0])
if len(sales_orders) > 1:
sales_orders = ", ".join(sales_orders)
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/patches.txt b/erpnext/patches.txt
index a25c7c2..c8cf7bc 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -263,6 +263,7 @@
erpnext.patches.v15_0.delete_saudi_doctypes
erpnext.patches.v14_0.show_loan_management_deprecation_warning
execute:frappe.rename_doc("Report", "TDS Payable Monthly", "Tax Withholding Details", force=True)
+erpnext.patches.v14_0.delete_education_module_portal_menu_items
[post_model_sync]
execute:frappe.delete_doc_if_exists('Workspace', 'ERPNext Integrations Settings')
diff --git a/erpnext/patches/v14_0/delete_education_doctypes.py b/erpnext/patches/v14_0/delete_education_doctypes.py
index 76b2300..56a596a 100644
--- a/erpnext/patches/v14_0/delete_education_doctypes.py
+++ b/erpnext/patches/v14_0/delete_education_doctypes.py
@@ -43,9 +43,18 @@
frappe.delete_doc("Number Card", card, ignore_missing=True, force=True)
doctypes = frappe.get_all("DocType", {"module": "education", "custom": 0}, pluck="name")
+
for doctype in doctypes:
frappe.delete_doc("DocType", doctype, ignore_missing=True)
+ portal_settings = frappe.get_doc("Portal Settings")
+
+ for row in portal_settings.get("menu"):
+ if row.reference_doctype in doctypes:
+ row.delete()
+
+ portal_settings.save()
+
frappe.delete_doc("Module Def", "Education", ignore_missing=True, force=True)
click.secho(
diff --git a/erpnext/patches/v14_0/delete_education_module_portal_menu_items.py b/erpnext/patches/v14_0/delete_education_module_portal_menu_items.py
new file mode 100644
index 0000000..d964f14
--- /dev/null
+++ b/erpnext/patches/v14_0/delete_education_module_portal_menu_items.py
@@ -0,0 +1,13 @@
+# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
+# License: MIT. See LICENSE
+
+import frappe
+
+
+def execute():
+ doctypes = frappe.get_all("DocType", {"module": "education", "custom": 0}, pluck="name")
+ items = frappe.get_all(
+ "Portal Menu Item", filters={"reference_doctype": ("in", doctypes)}, pluck="name"
+ )
+ for item in items:
+ frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True)
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index 502ee57..715b09c 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -453,7 +453,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2023-06-28 18:57:11.603497",
+ "modified": "2023-08-28 22:27:28.370849",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -475,7 +475,7 @@
"permlevel": 1,
"read": 1,
"report": 1,
- "role": "All"
+ "role": "Desk User"
},
{
"create": 1,
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 7d80ac1..c2ed579 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -10,9 +10,11 @@
from frappe.query_builder import Interval
from frappe.query_builder.functions import Count, CurDate, Date, UnixTimestamp
from frappe.utils import add_days, flt, get_datetime, get_time, get_url, nowtime, today
+from frappe.utils.user import is_website_user
from erpnext import get_default_company
from erpnext.controllers.queries import get_filters_cond
+from erpnext.controllers.website_list_for_contact import get_customers_suppliers
from erpnext.setup.doctype.holiday_list.holiday_list import is_holiday
@@ -318,9 +320,20 @@
def get_project_list(
doctype, txt, filters, limit_start, limit_page_length=20, order_by="modified"
):
+ user = frappe.session.user
+ customers, suppliers = get_customers_suppliers("Project", frappe.session.user)
+
+ ignore_permissions = False
+ if is_website_user():
+ if not filters:
+ filters = []
+
+ if customers:
+ filters.append([doctype, "customer", "in", customers])
+
+ ignore_permissions = True
+
meta = frappe.get_meta(doctype)
- if not filters:
- filters = []
fields = "distinct *"
@@ -351,18 +364,26 @@
limit_start=limit_start,
limit_page_length=limit_page_length,
order_by=order_by,
+ ignore_permissions=ignore_permissions,
)
def get_list_context(context=None):
- return {
- "show_sidebar": True,
- "show_search": True,
- "no_breadcrumbs": True,
- "title": _("Projects"),
- "get_list": get_project_list,
- "row_template": "templates/includes/projects/project_row.html",
- }
+ from erpnext.controllers.website_list_for_contact import get_list_context
+
+ list_context = get_list_context(context)
+ list_context.update(
+ {
+ "show_sidebar": True,
+ "show_search": True,
+ "no_breadcrumbs": True,
+ "title": _("Projects"),
+ "get_list": get_project_list,
+ "row_template": "templates/includes/projects/project_row.html",
+ }
+ )
+
+ return list_context
@frappe.whitelist()
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..582b487
--- /dev/null
+++ b/erpnext/public/js/bom_configurator/bom_configurator.bundle.js
@@ -0,0 +1,412 @@
+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 (node) {
+ 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);
+ node = node.parent_node;
+
+ }
+
+ }
+}
+
+frappe.ui.BOMConfigurator = BOMConfigurator;
\ No newline at end of file
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 c11d123..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) {
diff --git a/erpnext/quality_management/doctype/quality_action/quality_action.json b/erpnext/quality_management/doctype/quality_action/quality_action.json
index 0cc2a98..f0b33b9 100644
--- a/erpnext/quality_management/doctype/quality_action/quality_action.json
+++ b/erpnext/quality_management/doctype/quality_action/quality_action.json
@@ -91,7 +91,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-27 16:21:59.533937",
+ "modified": "2023-08-28 22:33:14.358143",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Action",
@@ -117,12 +117,13 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "All",
+ "role": "Desk User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json
index f3bd0dd..5fe6375 100644
--- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json
+++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.json
@@ -61,7 +61,7 @@
"link_fieldname": "feedback"
}
],
- "modified": "2020-10-27 16:20:10.918544",
+ "modified": "2023-08-28 22:21:36.144820",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Feedback",
@@ -87,12 +87,13 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "All",
+ "role": "Desk User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/quality_management/doctype/quality_goal/quality_goal.json b/erpnext/quality_management/doctype/quality_goal/quality_goal.json
index 2680255..f2b6ebc 100644
--- a/erpnext/quality_management/doctype/quality_goal/quality_goal.json
+++ b/erpnext/quality_management/doctype/quality_goal/quality_goal.json
@@ -76,7 +76,7 @@
"link_fieldname": "goal"
}
],
- "modified": "2020-10-27 15:57:59.368605",
+ "modified": "2023-08-28 22:33:27.718899",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Goal",
@@ -102,12 +102,13 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "All",
+ "role": "Desk User",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
index e2125c3..7ab28d8 100644
--- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
+++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
@@ -48,7 +48,7 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-02-27 16:36:45.657883",
+ "modified": "2023-08-28 22:33:57.447634",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Meeting",
@@ -74,7 +74,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "All",
+ "role": "Desk User",
"share": 1,
"write": 1
}
@@ -82,5 +82,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
index f5d7a6d..fd5a595 100644
--- a/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
+++ b/erpnext/quality_management/doctype/quality_procedure/quality_procedure.json
@@ -116,7 +116,7 @@
"link_fieldname": "procedure"
}
],
- "modified": "2023-08-29 12:49:53.963370",
+ "modified": "2023-08-28 22:33:36.483420",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Procedure",
@@ -143,7 +143,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "All",
+ "role": "Desk User",
"share": 1,
"write": 1
}
diff --git a/erpnext/quality_management/doctype/quality_review/quality_review.json b/erpnext/quality_management/doctype/quality_review/quality_review.json
index 31ad341..f38e8a5 100644
--- a/erpnext/quality_management/doctype/quality_review/quality_review.json
+++ b/erpnext/quality_management/doctype/quality_review/quality_review.json
@@ -84,7 +84,7 @@
"link_fieldname": "review"
}
],
- "modified": "2020-10-21 12:56:47.046172",
+ "modified": "2023-08-28 22:33:22.472980",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Review",
@@ -110,7 +110,7 @@
"print": 1,
"read": 1,
"report": 1,
- "role": "All",
+ "role": "Desk User",
"share": 1,
"write": 1
},
@@ -129,6 +129,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "goal",
"track_changes": 1
}
\ No newline at end of file
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/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/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index 2986087..e0f5090 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -233,7 +233,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2023-01-05 12:21:30.458628",
+ "modified": "2023-08-28 22:27:48.382985",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
@@ -266,7 +266,6 @@
"read": 1,
"report": 1,
"role": "Item Manager",
- "set_user_permissions": 1,
"share": 1,
"write": 1
},
@@ -296,7 +295,7 @@
"export": 1,
"print": 1,
"report": 1,
- "role": "All",
+ "role": "Desk User",
"select": 1,
"share": 1
}
diff --git a/erpnext/setup/doctype/print_heading/print_heading.json b/erpnext/setup/doctype/print_heading/print_heading.json
index dc07f0c..1083583 100644
--- a/erpnext/setup/doctype/print_heading/print_heading.json
+++ b/erpnext/setup/doctype/print_heading/print_heading.json
@@ -1,131 +1,68 @@
{
- "allow_copy": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:print_heading",
- "beta": 0,
- "creation": "2013-01-10 16:34:24",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Setup",
- "editable_grid": 0,
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:print_heading",
+ "creation": "2013-01-10 16:34:24",
+ "doctype": "DocType",
+ "document_type": "Setup",
+ "engine": "InnoDB",
+ "field_order": [
+ "print_heading",
+ "description"
+ ],
"fields": [
{
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "print_heading",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 1,
- "in_list_view": 1,
- "label": "Print Heading",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "print_heading",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "allow_on_submit": 1,
+ "fieldname": "print_heading",
+ "fieldtype": "Data",
+ "in_filter": 1,
+ "in_list_view": 1,
+ "label": "Print Heading",
+ "oldfieldname": "print_heading",
+ "oldfieldtype": "Data",
+ "reqd": 1,
+ "unique": 1
+ },
{
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "fieldname": "description",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_list_view": 1,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "description",
- "oldfieldtype": "Small Text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0,
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description",
+ "oldfieldname": "description",
+ "oldfieldtype": "Small Text",
"width": "300px"
}
- ],
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-font",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
-
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2016-07-25 05:24:25.628101",
- "modified_by": "Administrator",
- "module": "Setup",
- "name": "Print Heading",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-font",
+ "idx": 1,
+ "links": [],
+ "modified": "2023-08-28 22:17:42.041255",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Print Heading",
+ "naming_rule": "By fieldname",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "All",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "read": 1,
+ "role": "Desk User"
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "search_fields": "print_heading",
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "search_fields": "print_heading",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index 87c2a7e..756d004 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -912,7 +912,7 @@
"index_web_pages_for_search": 1,
"links": [],
"make_attachments_public": 1,
- "modified": "2023-07-14 17:18:18.658942",
+ "modified": "2023-08-28 22:16:40.305094",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@@ -971,7 +971,7 @@
"export": 1,
"print": 1,
"report": 1,
- "role": "All",
+ "role": "Desk User",
"select": 1,
"share": 1
}
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 111a0861..7f0dc2d 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -6,6 +6,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
import erpnext
@@ -19,19 +20,7 @@
self.set("items", [])
for pr in self.get("purchase_receipts"):
if pr.receipt_document_type and pr.receipt_document:
- pr_items = frappe.db.sql(
- """select pr_item.item_code, pr_item.description,
- pr_item.qty, pr_item.base_rate, pr_item.base_amount, pr_item.name,
- pr_item.cost_center, pr_item.is_fixed_asset
- from `tab{doctype} Item` pr_item where parent = %s
- and exists(select name from tabItem
- where name = pr_item.item_code and (is_stock_item = 1 or is_fixed_asset=1))
- """.format(
- doctype=pr.receipt_document_type
- ),
- pr.receipt_document,
- as_dict=True,
- )
+ pr_items = get_pr_items(pr)
for d in pr_items:
item = self.append("items")
@@ -247,3 +236,30 @@
),
tuple([item.valuation_rate] + serial_nos),
)
+
+
+def get_pr_items(purchase_receipt):
+ item = frappe.qb.DocType("Item")
+ pr_item = frappe.qb.DocType(purchase_receipt.receipt_document_type + " Item")
+ return (
+ frappe.qb.from_(pr_item)
+ .inner_join(item)
+ .on(item.name == pr_item.item_code)
+ .select(
+ pr_item.item_code,
+ pr_item.description,
+ pr_item.qty,
+ pr_item.base_rate,
+ pr_item.base_amount,
+ pr_item.name,
+ pr_item.cost_center,
+ pr_item.is_fixed_asset,
+ ConstantColumn(purchase_receipt.receipt_document_type).as_("receipt_document_type"),
+ ConstantColumn(purchase_receipt.receipt_document).as_("receipt_document"),
+ )
+ .where(
+ (pr_item.parent == purchase_receipt.receipt_document)
+ & ((item.is_stock_item == 1) | (item.is_fixed_asset == 1))
+ )
+ .run(as_dict=True)
+ )
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 0b5dc05..60aefdd 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -470,27 +470,28 @@
# Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries:
- for account, amount in landed_cost_entries[(d.item_code, d.name)].items():
- account_currency = get_account_currency(account)
- credit_amount = (
- flt(amount["base_amount"])
- if (amount["base_amount"] or account_currency != self.company_currency)
- else flt(amount["amount"])
- )
+ if (d.item_code, d.name) in landed_cost_entries:
+ for account, amount in landed_cost_entries[(d.item_code, d.name)].items():
+ account_currency = get_account_currency(account)
+ credit_amount = (
+ flt(amount["base_amount"])
+ if (amount["base_amount"] or account_currency != self.company_currency)
+ else flt(amount["amount"])
+ )
- self.add_gl_entry(
- gl_entries=gl_entries,
- account=account,
- cost_center=d.cost_center,
- debit=0.0,
- credit=credit_amount,
- remarks=remarks,
- against_account=warehouse_account_name,
- credit_in_account_currency=flt(amount["amount"]),
- account_currency=account_currency,
- project=d.project,
- item=d,
- )
+ self.add_gl_entry(
+ gl_entries=gl_entries,
+ account=account,
+ cost_center=d.cost_center,
+ debit=0.0,
+ credit=credit_amount,
+ remarks=remarks,
+ against_account=warehouse_account_name,
+ credit_in_account_currency=flt(amount["amount"]),
+ account_currency=account_currency,
+ project=d.project,
+ item=d,
+ )
if d.rate_difference_with_purchase_invoice and stock_rbnb:
account_currency = get_account_currency(stock_rbnb)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.js b/erpnext/stock/report/stock_balance/stock_balance.js
index 33ed955..6de5f00 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.js
+++ b/erpnext/stock/report/stock_balance/stock_balance.js
@@ -72,6 +72,14 @@
"options": "Warehouse Type"
},
{
+ "fieldname": "valuation_field_type",
+ "label": __("Valuation Field Type"),
+ "fieldtype": "Select",
+ "width": "80",
+ "options": "Currency\nFloat",
+ "default": "Currency"
+ },
+ {
"fieldname":"include_uom",
"label": __("Include UOM"),
"fieldtype": "Link",
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index d60e9b5..1dafb4d 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -446,9 +446,12 @@
{
"label": _("Valuation Rate"),
"fieldname": "val_rate",
- "fieldtype": "Float",
+ "fieldtype": self.filters.valuation_field_type or "Currency",
"width": 90,
"convertible": "rate",
+ "options": "Company:company:default_currency"
+ if self.filters.valuation_field_type == "Currency"
+ else None,
},
{
"label": _("Reserved Stock"),
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js
index 0def161..b00b422 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.js
@@ -82,7 +82,15 @@
"label": __("Include UOM"),
"fieldtype": "Link",
"options": "UOM"
- }
+ },
+ {
+ "fieldname": "valuation_field_type",
+ "label": __("Valuation Field Type"),
+ "fieldtype": "Select",
+ "width": "80",
+ "options": "Currency\nFloat",
+ "default": "Currency"
+ },
],
"formatter": function (value, row, column, data, default_formatter) {
value = default_formatter(value, row, column, data);
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index ed28ed3..eeef396 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -196,17 +196,21 @@
{
"label": _("Avg Rate (Balance Stock)"),
"fieldname": "valuation_rate",
- "fieldtype": "Float",
+ "fieldtype": filters.valuation_field_type,
"width": 180,
- "options": "Company:company:default_currency",
+ "options": "Company:company:default_currency"
+ if filters.valuation_field_type == "Currency"
+ else None,
"convertible": "rate",
},
{
"label": _("Valuation Rate"),
"fieldname": "in_out_rate",
- "fieldtype": "Float",
+ "fieldtype": filters.valuation_field_type,
"width": 140,
- "options": "Company:company:default_currency",
+ "options": "Company:company:default_currency"
+ if filters.valuation_field_type == "Currency"
+ else None,
"convertible": "rate",
},
{
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
index 0b14d4d..b7b3445 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/subcontracting_order.py
@@ -203,7 +203,10 @@
{
"Subcontracting Order": {
"doctype": "Subcontracting Receipt",
- "field_map": {"supplier_warehouse": "supplier_warehouse"},
+ "field_map": {
+ "supplier_warehouse": "supplier_warehouse",
+ "set_warehouse": "set_warehouse",
+ },
"validation": {
"docstatus": ["=", 1],
},
diff --git a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
index 6a2983f..22fdc13 100644
--- a/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
+++ b/erpnext/subcontracting/doctype/subcontracting_order/test_subcontracting_order.py
@@ -591,6 +591,13 @@
for idx, val in enumerate(sco.items):
val.warehouse = warehouses[idx]
+ warehouses = set()
+ for item in sco.items:
+ warehouses.add(item.warehouse)
+
+ if len(warehouses) == 1:
+ sco.set_warehouse = list(warehouses)[0]
+
if not args.do_not_save:
sco.insert()
if not args.do_not_submit:
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
index e374077..acf9553 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.js
@@ -22,7 +22,7 @@
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'));
@@ -34,7 +34,7 @@
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'));
}
@@ -94,7 +94,7 @@
company: frm.doc.company,
is_group: 0
}
- };
+ }
});
frm.set_query('rejected_warehouse', () => {
@@ -103,7 +103,7 @@
company: frm.doc.company,
is_group: 0
}
- };
+ }
});
frm.set_query('supplier_warehouse', () => {
@@ -112,7 +112,7 @@
company: frm.doc.company,
is_group: 0
}
- };
+ }
});
frm.set_query('warehouse', 'items', () => ({
@@ -129,10 +129,12 @@
}
}));
- frm.set_query('expense_account', 'items', () => ({
+ frm.set_query('expense_account', 'items', () => {
+ return {
query: 'erpnext.controllers.queries.get_expense_account',
filters: { 'company': frm.doc.company }
- }));
+ }
+ });
frm.set_query('batch_no', 'items', (doc, cdt, cdn) => {
var row = locals[cdt][cdn];
@@ -140,7 +142,7 @@
filters: {
item: row.item_code
}
- };
+ }
});
frm.set_query('batch_no', 'supplied_items', (doc, cdt, cdn) => {
@@ -149,7 +151,7 @@
filters: {
item: row.rm_item_code
}
- };
+ }
});
frm.set_query('serial_and_batch_bundle', 'supplied_items', (doc, cdt, cdn) => {
@@ -171,7 +173,7 @@
'item_code': row.doc.rm_item_code,
'voucher_type': frm.doc.doctype,
}
- };
+ }
}
let batch_no_field = frm.get_docfield('items', 'batch_no');
@@ -180,7 +182,7 @@
return {
'item': row.doc.item_code
}
- };
+ }
}
},
@@ -190,15 +192,37 @@
transaction_controller.setup_quality_inspection();
}
},
+
+ get_scrap_items: (frm) => {
+ frappe.call({
+ doc: frm.doc,
+ method: 'get_scrap_items',
+ args: {
+ recalculate_rate: true
+ },
+ freeze: true,
+ freeze_message: __('Getting Scrap Items'),
+ callback: (r) => {
+ if (!r.exc) {
+ frm.refresh();
+ }
+ }
+ });
+ },
});
frappe.ui.form.on('Landed Cost Taxes and Charges', {
amount: (frm, cdt, cdn) => {
+ set_missing_values(frm);
frm.events.set_base_amount(frm, cdt, cdn);
},
expense_account: (frm, cdt, cdn) => {
frm.events.set_account_currency(frm, cdt, cdn);
+ },
+
+ additional_costs_remove: (frm) => {
+ set_missing_values(frm);
}
});
@@ -214,6 +238,16 @@
rate(frm) {
set_missing_values(frm);
},
+
+ recalculate_rate(frm) {
+ if (frm.doc.recalculate_rate) {
+ set_missing_values(frm);
+ }
+ },
+
+ items_remove: (frm) => {
+ set_missing_values(frm);
+ },
});
frappe.ui.form.on('Subcontracting Receipt Supplied Item', {
@@ -225,7 +259,7 @@
let set_warehouse_in_children = (child_table, warehouse_field, warehouse) => {
let transaction_controller = new erpnext.TransactionController();
transaction_controller.autofill_warehouse(child_table, warehouse_field, warehouse);
-};
+}
let set_missing_values = (frm) => {
frappe.call({
@@ -235,4 +269,4 @@
if (!r.exc) frm.refresh();
},
});
-};
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
index 4b3cc83..8be1c1b 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.json
@@ -40,6 +40,7 @@
"col_break_warehouse",
"supplier_warehouse",
"items_section",
+ "get_scrap_items",
"items",
"section_break0",
"total_qty",
@@ -285,7 +286,7 @@
"reqd": 1
},
{
- "depends_on": "supplied_items",
+ "depends_on": "eval: (!doc.__islocal && doc.docstatus == 0 && doc.supplied_items)",
"fieldname": "get_current_stock",
"fieldtype": "Button",
"label": "Get Current Stock",
@@ -626,12 +627,19 @@
"fieldtype": "Check",
"label": "Edit Posting Date and Time",
"print_hide": 1
+ },
+ {
+ "depends_on": "eval: (!doc.__islocal && doc.docstatus == 0)",
+ "fieldname": "get_scrap_items",
+ "fieldtype": "Button",
+ "label": "Get Scrap Items",
+ "options": "get_scrap_items"
}
],
"in_create": 1,
"is_submittable": 1,
"links": [],
- "modified": "2023-07-06 18:43:16.171842",
+ "modified": "2023-08-26 10:52:04.050829",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt",
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
index afe1b60..8a12e3b 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/subcontracting_receipt.py
@@ -8,6 +8,7 @@
import erpnext
from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.subcontracting_controller import SubcontractingController
+from erpnext.stock.stock_ledger import get_valuation_rate
class SubcontractingReceipt(SubcontractingController):
@@ -36,33 +37,6 @@
),
)
- def update_status_updater_args(self):
- if cint(self.is_return):
- self.status_updater.extend(
- [
- {
- "source_dt": "Subcontracting Receipt Item",
- "target_dt": "Subcontracting Order Item",
- "join_field": "subcontracting_order_item",
- "target_field": "returned_qty",
- "source_field": "-1 * qty",
- "extra_cond": """ and exists (select name from `tabSubcontracting Receipt`
- where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""",
- },
- {
- "source_dt": "Subcontracting Receipt Item",
- "target_dt": "Subcontracting Receipt Item",
- "join_field": "subcontracting_receipt_item",
- "target_field": "returned_qty",
- "target_parent_dt": "Subcontracting Receipt",
- "target_parent_field": "per_returned",
- "target_ref_field": "received_qty",
- "source_field": "-1 * received_qty",
- "percent_join_field_parent": "return_against",
- },
- ]
- )
-
def before_validate(self):
super(SubcontractingReceipt, self).before_validate()
self.validate_items_qty()
@@ -71,15 +45,8 @@
self.set_items_expense_account()
def validate(self):
- if (
- frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
- == "BOM"
- ):
- self.supplied_items = []
- super(SubcontractingReceipt, self).validate()
- self.set_missing_values()
+ self.reset_supplied_items()
self.validate_posting_time()
- self.validate_rejected_warehouse()
if not self.get("is_return"):
self.validate_inspection()
@@ -87,15 +54,22 @@
if getdate(self.posting_date) > getdate(nowdate()):
frappe.throw(_("Posting Date cannot be future date"))
+ super(SubcontractingReceipt, self).validate()
+
+ if self.is_new() and self.get("_action") == "save" and not frappe.flags.in_test:
+ self.get_scrap_items()
+
+ self.set_missing_values()
+
+ if self.get("_action") == "submit":
+ self.validate_scrap_items()
+ self.validate_accepted_warehouse()
+ self.validate_rejected_warehouse()
+
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.get_current_stock()
- def on_update(self):
- for table_field in ["items", "supplied_items"]:
- if self.get(table_field):
- self.set_serial_and_batch_bundle(table_field)
-
def on_submit(self):
self.validate_available_qty_for_consumption()
self.update_status_updater_args()
@@ -107,6 +81,11 @@
self.repost_future_sle_and_gle()
self.update_status()
+ def on_update(self):
+ for table_field in ["items", "supplied_items"]:
+ if self.get(table_field):
+ self.set_serial_and_batch_bundle(table_field)
+
def on_cancel(self):
self.ignore_linked_doctypes = (
"GL Entry",
@@ -124,108 +103,6 @@
self.set_subcontracting_order_status()
self.update_status()
- @frappe.whitelist()
- def set_missing_values(self):
- self.calculate_additional_costs()
- self.calculate_supplied_items_qty_and_amount()
- self.calculate_items_qty_and_amount()
-
- def set_available_qty_for_consumption(self):
- supplied_items_details = {}
-
- sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item")
- for item in self.get("items"):
- supplied_items = (
- frappe.qb.from_(sco_supplied_item)
- .select(
- sco_supplied_item.rm_item_code,
- sco_supplied_item.reference_name,
- (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"),
- )
- .where(
- (sco_supplied_item.parent == item.subcontracting_order)
- & (sco_supplied_item.main_item_code == item.item_code)
- & (sco_supplied_item.reference_name == item.subcontracting_order_item)
- )
- ).run(as_dict=True)
-
- if supplied_items:
- supplied_items_details[item.name] = {}
-
- for supplied_item in supplied_items:
- supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty
- else:
- for item in self.get("supplied_items"):
- item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get(
- item.rm_item_code, 0
- )
-
- def calculate_supplied_items_qty_and_amount(self):
- for item in self.get("supplied_items") or []:
- item.amount = item.rate * item.consumed_qty
-
- self.set_available_qty_for_consumption()
-
- def calculate_items_qty_and_amount(self):
- rm_supp_cost = {}
- for item in self.get("supplied_items") or []:
- if item.reference_name in rm_supp_cost:
- rm_supp_cost[item.reference_name] += item.amount
- else:
- rm_supp_cost[item.reference_name] = item.amount
-
- total_qty = total_amount = 0
- for item in self.items:
- if item.qty and item.name in rm_supp_cost:
- item.rm_supp_cost = rm_supp_cost[item.name]
- item.rm_cost_per_qty = item.rm_supp_cost / item.qty
- rm_supp_cost.pop(item.name)
-
- if item.recalculate_rate:
- item.rate = (
- flt(item.rm_cost_per_qty) + flt(item.service_cost_per_qty) + flt(item.additional_cost_per_qty)
- )
-
- item.received_qty = item.qty + flt(item.rejected_qty)
- item.amount = item.qty * item.rate
- total_qty += item.qty
- total_amount += item.amount
- else:
- self.total_qty = total_qty
- self.total = total_amount
-
- def validate_rejected_warehouse(self):
- for item in self.items:
- if flt(item.rejected_qty) and not item.rejected_warehouse:
- if self.rejected_warehouse:
- item.rejected_warehouse = self.rejected_warehouse
-
- if not item.rejected_warehouse:
- frappe.throw(
- _("Row #{0}: Rejected Warehouse is mandatory for the rejected Item {1}").format(
- item.idx, item.item_code
- )
- )
-
- if item.get("rejected_warehouse") and (item.get("rejected_warehouse") == item.get("warehouse")):
- frappe.throw(
- _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx)
- )
-
- def validate_available_qty_for_consumption(self):
- for item in self.get("supplied_items"):
- precision = item.precision("consumed_qty")
- if (
- item.available_qty_for_consumption
- and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
- ):
- msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)}
- must be less than or equal to Available Qty For Consumption
- {flt(item.available_qty_for_consumption, precision)}
- in Consumed Items Table."""
-
- frappe.throw(_(msg))
-
def validate_items_qty(self):
for item in self.items:
if not (item.qty or item.rejected_qty):
@@ -267,6 +144,236 @@
if not item.expense_account:
item.expense_account = expense_account
+ def reset_supplied_items(self):
+ if (
+ frappe.db.get_single_value("Buying Settings", "backflush_raw_materials_of_subcontract_based_on")
+ == "BOM"
+ ):
+ self.supplied_items = []
+
+ @frappe.whitelist()
+ def get_scrap_items(self, recalculate_rate=False):
+ self.remove_scrap_items()
+
+ for item in list(self.items):
+ if item.bom:
+ bom = frappe.get_doc("BOM", item.bom)
+ for scrap_item in bom.scrap_items:
+ qty = flt(item.qty) * (flt(scrap_item.stock_qty) / flt(bom.quantity))
+ rate = (
+ get_valuation_rate(
+ scrap_item.item_code,
+ self.set_warehouse,
+ self.doctype,
+ self.name,
+ currency=erpnext.get_company_currency(self.company),
+ company=self.company,
+ )
+ or scrap_item.rate
+ )
+ self.append(
+ "items",
+ {
+ "is_scrap_item": 1,
+ "reference_name": item.name,
+ "item_code": scrap_item.item_code,
+ "item_name": scrap_item.item_name,
+ "qty": qty,
+ "stock_uom": scrap_item.stock_uom,
+ "recalculate_rate": 0,
+ "rate": rate,
+ "rm_cost_per_qty": 0,
+ "service_cost_per_qty": 0,
+ "additional_cost_per_qty": 0,
+ "scrap_cost_per_qty": 0,
+ "amount": qty * rate,
+ "warehouse": self.set_warehouse,
+ "rejected_warehouse": self.rejected_warehouse,
+ },
+ )
+
+ if recalculate_rate:
+ self.calculate_additional_costs()
+ self.calculate_items_qty_and_amount()
+
+ def remove_scrap_items(self, recalculate_rate=False):
+ for item in list(self.items):
+ if item.is_scrap_item:
+ self.remove(item)
+ else:
+ item.scrap_cost_per_qty = 0
+
+ if recalculate_rate:
+ self.calculate_items_qty_and_amount()
+
+ @frappe.whitelist()
+ def set_missing_values(self):
+ self.set_available_qty_for_consumption()
+ self.calculate_additional_costs()
+ self.calculate_items_qty_and_amount()
+
+ def set_available_qty_for_consumption(self):
+ supplied_items_details = {}
+
+ sco_supplied_item = frappe.qb.DocType("Subcontracting Order Supplied Item")
+ for item in self.get("items"):
+ supplied_items = (
+ frappe.qb.from_(sco_supplied_item)
+ .select(
+ sco_supplied_item.rm_item_code,
+ sco_supplied_item.reference_name,
+ (sco_supplied_item.total_supplied_qty - sco_supplied_item.consumed_qty).as_("available_qty"),
+ )
+ .where(
+ (sco_supplied_item.parent == item.subcontracting_order)
+ & (sco_supplied_item.main_item_code == item.item_code)
+ & (sco_supplied_item.reference_name == item.subcontracting_order_item)
+ )
+ ).run(as_dict=True)
+
+ if supplied_items:
+ supplied_items_details[item.name] = {}
+
+ for supplied_item in supplied_items:
+ supplied_items_details[item.name][supplied_item.rm_item_code] = supplied_item.available_qty
+ else:
+ for item in self.get("supplied_items"):
+ item.available_qty_for_consumption = supplied_items_details.get(item.reference_name, {}).get(
+ item.rm_item_code, 0
+ )
+
+ def calculate_items_qty_and_amount(self):
+ rm_cost_map = {}
+ for item in self.get("supplied_items") or []:
+ item.amount = flt(item.consumed_qty) * flt(item.rate)
+
+ if item.reference_name in rm_cost_map:
+ rm_cost_map[item.reference_name] += item.amount
+ else:
+ rm_cost_map[item.reference_name] = item.amount
+
+ scrap_cost_map = {}
+ for item in self.get("items") or []:
+ if item.is_scrap_item:
+ item.amount = flt(item.qty) * flt(item.rate)
+
+ if item.reference_name in scrap_cost_map:
+ scrap_cost_map[item.reference_name] += item.amount
+ else:
+ scrap_cost_map[item.reference_name] = item.amount
+
+ total_qty = total_amount = 0
+ for item in self.get("items") or []:
+ if not item.is_scrap_item:
+ if item.qty:
+ if item.name in rm_cost_map:
+ item.rm_supp_cost = rm_cost_map[item.name]
+ item.rm_cost_per_qty = item.rm_supp_cost / item.qty
+ rm_cost_map.pop(item.name)
+
+ if item.name in scrap_cost_map:
+ item.scrap_cost_per_qty = scrap_cost_map[item.name] / item.qty
+ scrap_cost_map.pop(item.name)
+ else:
+ item.scrap_cost_per_qty = 0
+
+ if item.recalculate_rate:
+ item.rate = (
+ flt(item.rm_cost_per_qty)
+ + flt(item.service_cost_per_qty)
+ + flt(item.additional_cost_per_qty)
+ - flt(item.scrap_cost_per_qty)
+ )
+
+ item.received_qty = flt(item.qty) + flt(item.rejected_qty)
+ item.amount = flt(item.qty) * flt(item.rate)
+
+ total_qty += flt(item.qty)
+ total_amount += item.amount
+ else:
+ self.total_qty = total_qty
+ self.total = total_amount
+
+ def validate_scrap_items(self):
+ for item in self.items:
+ if item.is_scrap_item:
+ if not item.qty:
+ frappe.throw(
+ _("Row #{0}: Scrap Item Qty cannot be zero").format(item.idx),
+ )
+
+ if item.rejected_qty:
+ frappe.throw(
+ _("Row #{0}: Rejected Qty cannot be set for Scrap Item {1}.").format(
+ item.idx, frappe.bold(item.item_code)
+ ),
+ )
+
+ if not item.reference_name:
+ frappe.throw(
+ _("Row #{0}: Finished Good reference is mandatory for Scrap Item {1}.").format(
+ item.idx, frappe.bold(item.item_code)
+ ),
+ )
+
+ def validate_accepted_warehouse(self):
+ for item in self.get("items"):
+ if flt(item.qty) and not item.warehouse:
+ if self.set_warehouse:
+ item.warehouse = self.set_warehouse
+ else:
+ frappe.throw(
+ _("Row #{0}: Accepted Warehouse is mandatory for the accepted Item {1}").format(
+ item.idx, item.item_code
+ )
+ )
+
+ if item.get("warehouse") and (item.get("warehouse") == item.get("rejected_warehouse")):
+ frappe.throw(
+ _("Row #{0}: Accepted Warehouse and Rejected Warehouse cannot be same").format(item.idx)
+ )
+
+ def validate_available_qty_for_consumption(self):
+ for item in self.get("supplied_items"):
+ precision = item.precision("consumed_qty")
+ if (
+ item.available_qty_for_consumption
+ and flt(item.available_qty_for_consumption, precision) - flt(item.consumed_qty, precision) < 0
+ ):
+ msg = f"""Row {item.idx}: Consumed Qty {flt(item.consumed_qty, precision)}
+ must be less than or equal to Available Qty For Consumption
+ {flt(item.available_qty_for_consumption, precision)}
+ in Consumed Items Table."""
+
+ frappe.throw(_(msg))
+
+ def update_status_updater_args(self):
+ if cint(self.is_return):
+ self.status_updater.extend(
+ [
+ {
+ "source_dt": "Subcontracting Receipt Item",
+ "target_dt": "Subcontracting Order Item",
+ "join_field": "subcontracting_order_item",
+ "target_field": "returned_qty",
+ "source_field": "-1 * qty",
+ "extra_cond": """ and exists (select name from `tabSubcontracting Receipt`
+ where name=`tabSubcontracting Receipt Item`.parent and is_return=1)""",
+ },
+ {
+ "source_dt": "Subcontracting Receipt Item",
+ "target_dt": "Subcontracting Receipt Item",
+ "join_field": "subcontracting_receipt_item",
+ "target_field": "returned_qty",
+ "target_parent_dt": "Subcontracting Receipt",
+ "target_parent_field": "per_returned",
+ "target_ref_field": "received_qty",
+ "source_field": "-1 * received_qty",
+ "percent_join_field_parent": "return_against",
+ },
+ ]
+ )
+
def update_status(self, status=None, update_modified=False):
if not status:
if self.docstatus == 0:
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
index a170527..1828f696 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt/test_subcontracting_receipt.py
@@ -23,6 +23,7 @@
make_subcontracted_items,
set_backflush_based_on,
)
+from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -507,8 +508,6 @@
self.assertEqual(scr.supplied_items[0].rate, sr.items[0].valuation_rate)
def test_subcontracting_receipt_raw_material_rate(self):
- from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
-
# Step - 1: Set Backflush Based On as "BOM"
set_backflush_based_on("BOM")
@@ -625,6 +624,77 @@
# ValidationError should not be raised as `Inspection Required before Purchase` is disabled
scr2.submit()
+ def test_scrap_items_for_subcontracting_receipt(self):
+ set_backflush_based_on("BOM")
+
+ fg_item = "Subcontracted Item SA1"
+
+ # Create Raw Materials
+ raw_materials = [
+ make_item(properties={"is_stock_item": 1, "valuation_rate": 100}).name,
+ make_item(properties={"is_stock_item": 1, "valuation_rate": 200}).name,
+ ]
+
+ # Create Scrap Items
+ scrap_item_1 = make_item(properties={"is_stock_item": 1, "valuation_rate": 10}).name
+ scrap_item_2 = make_item(properties={"is_stock_item": 1, "valuation_rate": 20}).name
+ scrap_items = [scrap_item_1, scrap_item_2]
+
+ service_items = [
+ {
+ "warehouse": "_Test Warehouse - _TC",
+ "item_code": "Subcontracted Service Item 1",
+ "qty": 10,
+ "rate": 100,
+ "fg_item": fg_item,
+ "fg_item_qty": 10,
+ },
+ ]
+
+ # Create BOM with Scrap Items
+ bom = make_bom(
+ item=fg_item, raw_materials=raw_materials, rate=100, currency="INR", do_not_submit=True
+ )
+ for idx, item in enumerate(bom.items):
+ item.qty = 1 * (idx + 1)
+ for idx, item in enumerate(scrap_items):
+ bom.append(
+ "scrap_items",
+ {
+ "item_code": item,
+ "stock_qty": 1 * (idx + 1),
+ "rate": 10 * (idx + 1),
+ },
+ )
+ bom.save()
+ bom.submit()
+
+ # Create PO and SCO
+ sco = get_subcontracting_order(service_items=service_items)
+
+ # Inward Raw Materials
+ rm_items = get_rm_items(sco.supplied_items)
+ itemwise_details = make_stock_in_entry(rm_items=rm_items)
+
+ # Transfer RM's to Subcontractor
+ make_stock_transfer_entry(
+ sco_no=sco.name,
+ rm_items=rm_items,
+ itemwise_details=copy.deepcopy(itemwise_details),
+ )
+
+ # Create Subcontracting Receipt
+ scr = make_subcontracting_receipt(sco.name)
+ scr.save()
+ scr.get_scrap_items()
+
+ # Test - 1: Scrap Items should be fetched from BOM in items table with `is_scrap_item` = 1
+ scr_scrap_items = set([item.item_code for item in scr.items if item.is_scrap_item])
+ self.assertEqual(len(scr.items), 3) # 1 FG Item + 2 Scrap Items
+ self.assertEqual(scr_scrap_items, set(scrap_items))
+
+ scr.submit()
+
def make_return_subcontracting_receipt(**args):
args = frappe._dict(args)
diff --git a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
index d728780..c036390 100644
--- a/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
+++ b/erpnext/subcontracting/doctype/subcontracting_receipt_item/subcontracting_receipt_item.json
@@ -10,6 +10,7 @@
"item_code",
"column_break_2",
"item_name",
+ "is_scrap_item",
"section_break_4",
"description",
"brand",
@@ -24,8 +25,6 @@
"col_break2",
"stock_uom",
"conversion_factor",
- "tracking_section",
- "col_break_tracking_section",
"rate_and_amount",
"rate",
"amount",
@@ -34,18 +33,20 @@
"rm_cost_per_qty",
"service_cost_per_qty",
"additional_cost_per_qty",
+ "scrap_cost_per_qty",
"rm_supp_cost",
"warehouse_and_reference",
"warehouse",
- "rejected_warehouse",
"subcontracting_order",
- "column_break_40",
- "schedule_date",
- "quality_inspection",
"subcontracting_order_item",
"subcontracting_receipt_item",
- "section_break_45",
+ "column_break_40",
+ "rejected_warehouse",
"bom",
+ "quality_inspection",
+ "schedule_date",
+ "reference_name",
+ "section_break_45",
"serial_and_batch_bundle",
"serial_no",
"col_break5",
@@ -85,12 +86,13 @@
"fieldtype": "Column Break"
},
{
+ "fetch_from": "item_code.item_name",
+ "fetch_if_empty": 1,
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Item Name",
- "print_hide": 1,
- "reqd": 1
+ "print_hide": 1
},
{
"collapsible": 1,
@@ -99,11 +101,12 @@
"label": "Description"
},
{
+ "fetch_from": "item_code.description",
+ "fetch_if_empty": 1,
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description",
"print_width": "300px",
- "reqd": 1,
"width": "300px"
},
{
@@ -157,6 +160,7 @@
"no_copy": 1,
"print_hide": 1,
"print_width": "100px",
+ "read_only_depends_on": "eval: doc.is_scrap_item",
"width": "100px"
},
{
@@ -214,6 +218,8 @@
"fieldtype": "Column Break"
},
{
+ "default": "0",
+ "depends_on": "eval: !doc.is_scrap_item",
"fieldname": "rm_cost_per_qty",
"fieldtype": "Currency",
"label": "Raw Material Cost Per Qty",
@@ -221,6 +227,8 @@
"read_only": 1
},
{
+ "default": "0",
+ "depends_on": "eval: !doc.is_scrap_item",
"fieldname": "service_cost_per_qty",
"fieldtype": "Currency",
"label": "Service Cost Per Qty",
@@ -229,6 +237,7 @@
},
{
"default": "0",
+ "depends_on": "eval: !doc.is_scrap_item",
"fieldname": "additional_cost_per_qty",
"fieldtype": "Currency",
"label": "Additional Cost Per Qty",
@@ -260,6 +269,7 @@
"options": "Warehouse",
"print_hide": 1,
"print_width": "100px",
+ "read_only_depends_on": "eval: doc.is_scrap_item",
"width": "100px"
},
{
@@ -295,7 +305,8 @@
},
{
"fieldname": "section_break_45",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Serial and Batch Details"
},
{
"depends_on": "eval:!doc.is_fixed_asset",
@@ -321,7 +332,8 @@
"fieldtype": "Small Text",
"label": "Rejected Serial No",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"fieldname": "subcontracting_order_item",
@@ -345,7 +357,8 @@
"label": "BOM",
"no_copy": 1,
"options": "BOM",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only_depends_on": "eval: doc.is_scrap_item"
},
{
"fetch_from": "item_code.brand",
@@ -411,14 +424,6 @@
"fieldtype": "Column Break"
},
{
- "fieldname": "tracking_section",
- "fieldtype": "Section Break"
- },
- {
- "fieldname": "col_break_tracking_section",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
@@ -456,6 +461,7 @@
"print_hide": 1
},
{
+ "default": "0",
"depends_on": "returned_qty",
"fieldname": "returned_qty",
"fieldtype": "Float",
@@ -471,9 +477,11 @@
},
{
"default": "1",
+ "depends_on": "eval: !doc.is_scrap_item",
"fieldname": "recalculate_rate",
"fieldtype": "Check",
- "label": "Recalculate Rate"
+ "label": "Recalculate Rate",
+ "read_only_depends_on": "eval: doc.is_scrap_item"
},
{
"fieldname": "serial_and_batch_bundle",
@@ -490,12 +498,40 @@
"no_copy": 1,
"options": "Serial and Batch Bundle",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: !doc.bom",
+ "fieldname": "is_scrap_item",
+ "fieldtype": "Check",
+ "label": "Is Scrap Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only_depends_on": "eval: doc.bom"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: !doc.is_scrap_item",
+ "fieldname": "scrap_cost_per_qty",
+ "fieldtype": "Float",
+ "label": "Scrap Cost Per Qty",
+ "no_copy": 1,
+ "non_negative": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "reference_name",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Reference Name",
+ "no_copy": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2023-07-06 18:43:45.599761",
+ "modified": "2023-08-25 20:09:03.069417",
"modified_by": "Administrator",
"module": "Subcontracting",
"name": "Subcontracting Receipt Item",
diff --git a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
index 1c6f24b..5882033 100644
--- a/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
+++ b/erpnext/support/doctype/service_level_agreement/service_level_agreement.json
@@ -192,7 +192,7 @@
}
],
"links": [],
- "modified": "2023-04-21 17:16:56.192560",
+ "modified": "2023-08-28 22:17:54.740924",
"modified_by": "Administrator",
"module": "Support",
"name": "Service Level Agreement",
@@ -213,7 +213,7 @@
},
{
"read": 1,
- "role": "All"
+ "role": "Desk User"
}
],
"sort_field": "modified",
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