Merge branch 'develop' of https://github.com/frappe/erpnext into opening_entry
This commit is contained in:
commit
d11a64ddcb
5
.github/helper/install.sh
vendored
5
.github/helper/install.sh
vendored
@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
|
||||
frappeuser=${FRAPPE_USER:-"frappe"}
|
||||
frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
|
||||
frappebranch=${FRAPPE_BRANCH:-$githubbranch}
|
||||
|
||||
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
|
||||
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
|
||||
@ -60,7 +61,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile
|
||||
sed -i 's/socketio:/# socketio:/g' Procfile
|
||||
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
|
||||
|
||||
bench get-app payments
|
||||
bench get-app payments --branch ${githubbranch%"-hotfix"}
|
||||
bench get-app erpnext "${GITHUB_WORKSPACE}"
|
||||
|
||||
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
|
||||
|
@ -394,7 +394,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda
|
||||
|
||||
if ancestors and not allow_independent_account_creation:
|
||||
for ancestor in ancestors:
|
||||
if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
|
||||
old_name = frappe.db.get_value(
|
||||
"Account",
|
||||
{"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor},
|
||||
"name",
|
||||
)
|
||||
|
||||
if old_name:
|
||||
# same account in parent company exists
|
||||
allow_child_account_creation = _("Allow Account Creation Against Child Company")
|
||||
|
||||
|
@ -221,12 +221,15 @@ class PaymentReconciliation(Document):
|
||||
|
||||
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
|
||||
difference_amount = 0
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
||||
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
if frappe.get_cached_value(
|
||||
"Account", self.receivable_payable_account, "account_currency"
|
||||
) != frappe.get_cached_value("Company", self.company, "default_currency"):
|
||||
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
|
||||
"exchange_rate", 1
|
||||
):
|
||||
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
|
||||
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
|
||||
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
|
||||
|
||||
return difference_amount
|
||||
|
||||
|
@ -5,7 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import add_days, flt, nowdate
|
||||
|
||||
from erpnext import get_default_cost_center
|
||||
@ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
si.reload()
|
||||
@ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# check PR tool output
|
||||
@ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
@ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
# Difference amount should not be calculated for base currency accounts
|
||||
for row in pr.allocation:
|
||||
self.assertEqual(flt(row.get("difference_amount")), 0.0)
|
||||
|
||||
pr.reconcile()
|
||||
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
@ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase):
|
||||
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
|
||||
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
|
||||
|
||||
@change_settings(
|
||||
"Accounts Settings",
|
||||
{
|
||||
"allow_multi_currency_invoices_against_single_party_account": 1,
|
||||
},
|
||||
)
|
||||
def test_no_difference_amount_for_base_currency_accounts(self):
|
||||
# Make Sale Invoice
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = 85
|
||||
si.debit_to = self.debit_to
|
||||
si.save().submit()
|
||||
|
||||
# Make payment using Payment Entry
|
||||
pe1 = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=self.customer,
|
||||
paid_from=self.debit_to,
|
||||
paid_to=self.bank,
|
||||
paid_amount=100,
|
||||
)
|
||||
|
||||
pe1.save()
|
||||
pe1.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = self.debit_to
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
invoices = [x.as_dict() for x in pr.invoices]
|
||||
payments = [pr.payments[0].as_dict()]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
|
||||
self.assertEqual(pr.allocation[0].allocated_amount, 85)
|
||||
self.assertEqual(pr.allocation[0].difference_amount, 0)
|
||||
|
||||
|
||||
def make_customer(customer_name, currency=None):
|
||||
if not frappe.db.exists("Customer", customer_name):
|
||||
|
@ -859,7 +859,7 @@ class ReceivablePayableReport(object):
|
||||
)
|
||||
else:
|
||||
self.qb_selection_filter.append(
|
||||
self.ple[dimension.fieldname] == self.filters[dimension.fieldname]
|
||||
self.ple[dimension.fieldname].isin(self.filters[dimension.fieldname])
|
||||
)
|
||||
|
||||
def is_invoice(self, ple):
|
||||
|
@ -501,7 +501,14 @@ class GrossProfitGenerator(object):
|
||||
):
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
row.qty += flt(returned_item_row.qty)
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
returned_item_row.qty = 0
|
||||
else:
|
||||
row.qty = 0
|
||||
returned_item_row.qty += row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
if flt(row.qty) or row.base_amount:
|
||||
@ -734,6 +741,8 @@ class GrossProfitGenerator(object):
|
||||
if self.filters.to_date:
|
||||
conditions += " and posting_date <= %(to_date)s"
|
||||
|
||||
conditions += " and (is_return = 0 or (is_return=1 and return_against is null))"
|
||||
|
||||
if self.filters.item_group:
|
||||
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))
|
||||
|
||||
|
@ -381,3 +381,82 @@ class TestGrossProfit(FrappeTestCase):
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
|
||||
def test_crnote_against_invoice_with_multiple_instances_of_same_item(self):
|
||||
"""
|
||||
Item Qty for Sales Invoices with multiple instances of same item go in the -ve. Ideally, the credit noteshould cancel out the invoice items.
|
||||
"""
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
|
||||
|
||||
# Invoice with an item added twice
|
||||
sinv = self.create_sales_invoice(qty=1, rate=100, posting_date=nowdate(), do_not_submit=True)
|
||||
sinv.append("items", frappe.copy_doc(sinv.items[0], ignore_no_copy=False))
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
# Create Credit Note for Invoice
|
||||
cr_note = make_sales_return(sinv.name)
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
expected_entry = {
|
||||
"parent_invoice": sinv.name,
|
||||
"currency": "INR",
|
||||
"sales_invoice": self.item,
|
||||
"customer": self.customer,
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 0.0,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
# Both items of Invoice should have '0' qty
|
||||
self.assertEqual(len(gp_entry), 2)
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[1])
|
||||
|
||||
def test_standalone_cr_notes(self):
|
||||
"""
|
||||
Standalone cr notes will be reported as usual
|
||||
"""
|
||||
# Make Cr Note
|
||||
sinv = self.create_sales_invoice(
|
||||
qty=-1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
|
||||
)
|
||||
sinv.is_return = 1
|
||||
sinv = sinv.save().submit()
|
||||
|
||||
filters = frappe._dict(
|
||||
company=self.company, from_date=nowdate(), to_date=nowdate(), group_by="Invoice"
|
||||
)
|
||||
|
||||
columns, data = execute(filters=filters)
|
||||
expected_entry = {
|
||||
"parent_invoice": sinv.name,
|
||||
"currency": "INR",
|
||||
"sales_invoice": self.item,
|
||||
"customer": self.customer,
|
||||
"posting_date": frappe.utils.datetime.date.fromisoformat(nowdate()),
|
||||
"item_code": self.item,
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": -1.0,
|
||||
"avg._selling_rate": 100.0,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
self.assertDictContainsSubset(expected_entry, gp_entry[0])
|
||||
|
@ -79,7 +79,6 @@ def validate_filters(filters):
|
||||
|
||||
|
||||
def get_data(filters):
|
||||
|
||||
accounts = frappe.db.sql(
|
||||
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
|
||||
|
||||
@ -119,12 +118,10 @@ def get_data(filters):
|
||||
ignore_closing_entries=not flt(filters.with_period_closing_entry),
|
||||
)
|
||||
|
||||
total_row = calculate_values(
|
||||
accounts, gl_entries_by_account, opening_balances, filters, company_currency
|
||||
)
|
||||
calculate_values(accounts, gl_entries_by_account, opening_balances)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name)
|
||||
|
||||
data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency)
|
||||
data = prepare_data(accounts, filters, parent_children_map, company_currency)
|
||||
data = filter_out_zero_value_rows(
|
||||
data, parent_children_map, show_zero_values=filters.get("show_zero_values")
|
||||
)
|
||||
@ -266,7 +263,7 @@ def get_opening_balance(
|
||||
return gle
|
||||
|
||||
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency):
|
||||
def calculate_values(accounts, gl_entries_by_account, opening_balances):
|
||||
init = {
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
@ -276,22 +273,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
||||
"closing_credit": 0.0,
|
||||
}
|
||||
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
d.update(init.copy())
|
||||
|
||||
@ -309,8 +290,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
|
||||
|
||||
prepare_opening_closing(d)
|
||||
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
def calculate_total_row(accounts, company_currency):
|
||||
total_row = {
|
||||
"account": "'" + _("Total") + "'",
|
||||
"account_name": "'" + _("Total") + "'",
|
||||
"warn_if_negative": True,
|
||||
"opening_debit": 0.0,
|
||||
"opening_credit": 0.0,
|
||||
"debit": 0.0,
|
||||
"credit": 0.0,
|
||||
"closing_debit": 0.0,
|
||||
"closing_credit": 0.0,
|
||||
"parent_account": None,
|
||||
"indent": 0,
|
||||
"has_value": True,
|
||||
"currency": company_currency,
|
||||
}
|
||||
|
||||
for d in accounts:
|
||||
if not d.parent_account:
|
||||
for field in value_fields:
|
||||
total_row[field] += d[field]
|
||||
|
||||
return total_row
|
||||
|
||||
@ -322,7 +323,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
|
||||
accounts_by_name[d.parent_account][key] += d[key]
|
||||
|
||||
|
||||
def prepare_data(accounts, filters, total_row, parent_children_map, company_currency):
|
||||
def prepare_data(accounts, filters, parent_children_map, company_currency):
|
||||
data = []
|
||||
|
||||
for d in accounts:
|
||||
@ -353,6 +354,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
|
||||
row["has_value"] = has_value
|
||||
data.append(row)
|
||||
|
||||
total_row = calculate_total_row(accounts, company_currency)
|
||||
data.extend([{}, total_row])
|
||||
|
||||
return data
|
||||
|
@ -455,7 +455,9 @@ def reconcile_against_document(args): # nosemgrep
|
||||
try:
|
||||
doc.validate_total_debit_and_credit()
|
||||
except Exception as validation_exception:
|
||||
raise frappe.ValidationError(_(f"Validation Error for {doc.name}")) from validation_exception
|
||||
raise frappe.ValidationError(
|
||||
_("Validation Error for {0}").format(doc.name)
|
||||
) from validation_exception
|
||||
|
||||
doc.save(ignore_permissions=True)
|
||||
# re-submit advance entry
|
||||
|
@ -828,8 +828,8 @@ class TestDepreciationMethods(AssetSetup):
|
||||
expected_schedules = [
|
||||
["2030-12-31", 28630.14, 28630.14],
|
||||
["2031-12-31", 35684.93, 64315.07],
|
||||
["2032-12-31", 17842.47, 82157.54],
|
||||
["2033-06-06", 5342.46, 87500.0],
|
||||
["2032-12-31", 17842.46, 82157.53],
|
||||
["2033-06-06", 5342.47, 87500.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
|
@ -140,8 +140,8 @@ class AssetDepreciationSchedule(Document):
|
||||
self.asset = asset_doc.name
|
||||
self.finance_book = row.finance_book
|
||||
self.finance_book_id = row.idx
|
||||
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation
|
||||
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked
|
||||
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
|
||||
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
|
||||
self.gross_purchase_amount = asset_doc.gross_purchase_amount
|
||||
self.depreciation_method = row.depreciation_method
|
||||
self.total_number_of_depreciations = row.total_number_of_depreciations
|
||||
@ -185,14 +185,14 @@ class AssetDepreciationSchedule(Document):
|
||||
):
|
||||
asset_doc.validate_asset_finance_books(row)
|
||||
|
||||
value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
|
||||
value_after_depreciation = self._get_value_after_depreciation_for_making_schedule(asset_doc, row)
|
||||
row.value_after_depreciation = value_after_depreciation
|
||||
|
||||
if update_asset_finance_book_row:
|
||||
row.db_update()
|
||||
|
||||
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint(
|
||||
asset_doc.number_of_depreciations_booked
|
||||
self.number_of_depreciations_booked
|
||||
)
|
||||
|
||||
has_pro_rata = asset_doc.check_is_pro_rata(row)
|
||||
@ -235,13 +235,12 @@ class AssetDepreciationSchedule(Document):
|
||||
self.add_depr_schedule_row(
|
||||
date_of_disposal,
|
||||
depreciation_amount,
|
||||
row.depreciation_method,
|
||||
)
|
||||
|
||||
break
|
||||
|
||||
# For first row
|
||||
if has_pro_rata and not asset_doc.opening_accumulated_depreciation and n == 0:
|
||||
if has_pro_rata and not self.opening_accumulated_depreciation and n == 0:
|
||||
from_date = add_days(
|
||||
asset_doc.available_for_use_date, -1
|
||||
) # needed to calc depr amount for available_for_use_date too
|
||||
@ -260,7 +259,7 @@ class AssetDepreciationSchedule(Document):
|
||||
# In case of increase_in_asset_life, the asset.to_date is already set on asset_repair submission
|
||||
asset_doc.to_date = add_months(
|
||||
asset_doc.available_for_use_date,
|
||||
(n + asset_doc.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
|
||||
(n + self.number_of_depreciations_booked) * cint(row.frequency_of_depreciation),
|
||||
)
|
||||
|
||||
depreciation_amount_without_pro_rata = depreciation_amount
|
||||
@ -298,7 +297,6 @@ class AssetDepreciationSchedule(Document):
|
||||
self.add_depr_schedule_row(
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
row.depreciation_method,
|
||||
)
|
||||
|
||||
# to ensure that final accumulated depreciation amount is accurate
|
||||
@ -325,14 +323,12 @@ class AssetDepreciationSchedule(Document):
|
||||
self,
|
||||
schedule_date,
|
||||
depreciation_amount,
|
||||
depreciation_method,
|
||||
):
|
||||
self.append(
|
||||
"depreciation_schedule",
|
||||
{
|
||||
"schedule_date": schedule_date,
|
||||
"depreciation_amount": depreciation_amount,
|
||||
"depreciation_method": depreciation_method,
|
||||
},
|
||||
)
|
||||
|
||||
@ -346,7 +342,7 @@ class AssetDepreciationSchedule(Document):
|
||||
straight_line_idx = [
|
||||
d.idx
|
||||
for d in self.get("depreciation_schedule")
|
||||
if d.depreciation_method == "Straight Line" or d.depreciation_method == "Manual"
|
||||
if self.depreciation_method == "Straight Line" or self.depreciation_method == "Manual"
|
||||
]
|
||||
|
||||
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
|
||||
@ -377,16 +373,15 @@ class AssetDepreciationSchedule(Document):
|
||||
accumulated_depreciation, d.precision("accumulated_depreciation_amount")
|
||||
)
|
||||
|
||||
def _get_value_after_depreciation_for_making_schedule(self, asset_doc, fb_row):
|
||||
if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
|
||||
value_after_depreciation = flt(fb_row.value_after_depreciation)
|
||||
else:
|
||||
value_after_depreciation = flt(self.gross_purchase_amount) - flt(
|
||||
self.opening_accumulated_depreciation
|
||||
)
|
||||
|
||||
def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
|
||||
if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
|
||||
value_after_depreciation = flt(fb_row.value_after_depreciation)
|
||||
else:
|
||||
value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt(
|
||||
asset_doc.opening_accumulated_depreciation
|
||||
)
|
||||
|
||||
return value_after_depreciation
|
||||
return value_after_depreciation
|
||||
|
||||
|
||||
def make_draft_asset_depr_schedules_if_not_present(asset_doc):
|
||||
|
@ -12,8 +12,7 @@
|
||||
"column_break_3",
|
||||
"accumulated_depreciation_amount",
|
||||
"journal_entry",
|
||||
"make_depreciation_entry",
|
||||
"depreciation_method"
|
||||
"make_depreciation_entry"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -58,20 +57,11 @@
|
||||
"fieldname": "make_depreciation_entry",
|
||||
"fieldtype": "Button",
|
||||
"label": "Make Depreciation Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "depreciation_method",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 1,
|
||||
"label": "Depreciation Method",
|
||||
"options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-12-06 20:35:50.264281",
|
||||
"modified": "2023-03-13 23:17:15.849950",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Depreciation Schedule",
|
||||
|
@ -16,6 +16,7 @@
|
||||
"transaction_settings_section",
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"over_order_allowance",
|
||||
"column_break_12",
|
||||
"maintain_same_rate",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
@ -156,6 +157,13 @@
|
||||
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"fieldtype": "Check",
|
||||
"label": "Set Landed Cost Based on Purchase Invoice Rate"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
|
||||
"fieldname": "over_order_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Order Allowance (%)"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@ -163,7 +171,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-28 15:41:32.686805",
|
||||
"modified": "2023-03-02 17:02:14.404622",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
@ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
|
||||
from erpnext.accounts.party import get_party_account, get_party_account_currency
|
||||
from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items
|
||||
from erpnext.controllers.buying_controller import BuyingController
|
||||
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
)
|
||||
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details
|
||||
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty
|
||||
@ -69,6 +72,7 @@ class PurchaseOrder(BuyingController):
|
||||
self.validate_with_previous_doc()
|
||||
self.validate_for_subcontracting()
|
||||
self.validate_minimum_order_qty()
|
||||
validate_against_blanket_order(self)
|
||||
|
||||
if self.is_old_subcontracting_flow:
|
||||
self.validate_bom_for_subcontracting_items()
|
||||
|
@ -226,11 +226,11 @@ class TestWebsiteItem(unittest.TestCase):
|
||||
self.assertTrue(bool(data.product_info["price"]))
|
||||
|
||||
price_object = data.product_info["price"]
|
||||
self.assertEqual(price_object.get("discount_percent"), 25)
|
||||
self.assertEqual(price_object.get("discount_percent"), 25.0)
|
||||
self.assertEqual(price_object.get("price_list_rate"), 750)
|
||||
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00")
|
||||
self.assertEqual(price_object.get("formatted_price"), "₹ 750.00")
|
||||
self.assertEqual(price_object.get("formatted_discount_percent"), "25%")
|
||||
self.assertEqual(price_object.get("formatted_discount_percent"), "25.0%")
|
||||
|
||||
# switch to admin and disable show price
|
||||
frappe.set_user("Administrator")
|
||||
|
@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', {
|
||||
},
|
||||
|
||||
setup: function(frm) {
|
||||
frm.custom_make_buttons = {
|
||||
'Purchase Order': 'Purchase Order',
|
||||
'Sales Order': 'Sales Order',
|
||||
'Quotation': 'Quotation',
|
||||
};
|
||||
|
||||
frm.add_fetch("customer", "customer_name", "customer_name");
|
||||
frm.add_fetch("supplier", "supplier_name", "supplier_name");
|
||||
},
|
||||
|
@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, getdate
|
||||
|
||||
from erpnext.stock.doctype.item.item import get_item_defaults
|
||||
@ -29,21 +30,23 @@ class BlanketOrder(Document):
|
||||
|
||||
def update_ordered_qty(self):
|
||||
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order"
|
||||
|
||||
trans = frappe.qb.DocType(ref_doctype)
|
||||
trans_item = frappe.qb.DocType(f"{ref_doctype} Item")
|
||||
|
||||
item_ordered_qty = frappe._dict(
|
||||
frappe.db.sql(
|
||||
"""
|
||||
select trans_item.item_code, sum(trans_item.stock_qty) as qty
|
||||
from `tab{0} Item` trans_item, `tab{0}` trans
|
||||
where trans.name = trans_item.parent
|
||||
and trans_item.blanket_order=%s
|
||||
and trans.docstatus=1
|
||||
and trans.status not in ('Closed', 'Stopped')
|
||||
group by trans_item.item_code
|
||||
""".format(
|
||||
ref_doctype
|
||||
),
|
||||
self.name,
|
||||
)
|
||||
(
|
||||
frappe.qb.from_(trans_item)
|
||||
.from_(trans)
|
||||
.select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty"))
|
||||
.where(
|
||||
(trans.name == trans_item.parent)
|
||||
& (trans_item.blanket_order == self.name)
|
||||
& (trans.docstatus == 1)
|
||||
& (trans.status.notin(["Stopped", "Closed"]))
|
||||
)
|
||||
.groupby(trans_item.item_code)
|
||||
).run()
|
||||
)
|
||||
|
||||
for d in self.items:
|
||||
@ -79,7 +82,43 @@ def make_order(source_name):
|
||||
"doctype": doctype + " Item",
|
||||
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0,
|
||||
},
|
||||
},
|
||||
)
|
||||
return target_doc
|
||||
|
||||
|
||||
def validate_against_blanket_order(order_doc):
|
||||
if order_doc.doctype in ("Sales Order", "Purchase Order"):
|
||||
order_data = {}
|
||||
|
||||
for item in order_doc.get("items"):
|
||||
if item.against_blanket_order and item.blanket_order:
|
||||
if item.blanket_order in order_data:
|
||||
if item.item_code in order_data[item.blanket_order]:
|
||||
order_data[item.blanket_order][item.item_code] += item.qty
|
||||
else:
|
||||
order_data[item.blanket_order][item.item_code] = item.qty
|
||||
else:
|
||||
order_data[item.blanket_order] = {item.item_code: item.qty}
|
||||
|
||||
if order_data:
|
||||
allowance = flt(
|
||||
frappe.db.get_single_value(
|
||||
"Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings",
|
||||
"over_order_allowance",
|
||||
)
|
||||
)
|
||||
for bo_name, item_data in order_data.items():
|
||||
bo_doc = frappe.get_doc("Blanket Order", bo_name)
|
||||
for item in bo_doc.get("items"):
|
||||
if item.item_code in item_data:
|
||||
remaining_qty = item.qty - item.ordered_qty
|
||||
allowed_qty = remaining_qty + (remaining_qty * (allowance / 100))
|
||||
if allowed_qty < item_data[item.item_code]:
|
||||
frappe.throw(
|
||||
_("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format(
|
||||
item.item_code, allowed_qty, bo_name
|
||||
)
|
||||
)
|
||||
|
@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase):
|
||||
po1.currency = get_company_currency(po1.company)
|
||||
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty))
|
||||
|
||||
def test_over_order_allowance(self):
|
||||
# Sales Order
|
||||
bo = make_blanket_order(blanket_order_type="Selling", quantity=100)
|
||||
|
||||
frappe.flags.args.doctype = "Sales Order"
|
||||
so = make_order(bo.name)
|
||||
so.currency = get_company_currency(so.company)
|
||||
so.delivery_date = today()
|
||||
so.items[0].qty = 110
|
||||
self.assertRaises(frappe.ValidationError, so.submit)
|
||||
|
||||
frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10)
|
||||
so.submit()
|
||||
|
||||
# Purchase Order
|
||||
bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100)
|
||||
|
||||
frappe.flags.args.doctype = "Purchase Order"
|
||||
po = make_order(bo.name)
|
||||
po.currency = get_company_currency(po.company)
|
||||
po.schedule_date = today()
|
||||
po.items[0].qty = 110
|
||||
self.assertRaises(frappe.ValidationError, po.submit)
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
|
||||
po.submit()
|
||||
|
||||
|
||||
def make_blanket_order(**args):
|
||||
args = frappe._dict(args)
|
||||
|
@ -31,7 +31,7 @@ class BOMTree:
|
||||
|
||||
# specifying the attributes to save resources
|
||||
# ref: https://docs.python.org/3/reference/datamodel.html#slots
|
||||
__slots__ = ["name", "child_items", "is_bom", "item_code", "exploded_qty", "qty"]
|
||||
__slots__ = ["name", "child_items", "is_bom", "item_code", "qty", "exploded_qty", "bom_qty"]
|
||||
|
||||
def __init__(
|
||||
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1
|
||||
@ -50,9 +50,10 @@ class BOMTree:
|
||||
def __create_tree(self):
|
||||
bom = frappe.get_cached_doc("BOM", self.name)
|
||||
self.item_code = bom.item
|
||||
self.bom_qty = bom.quantity
|
||||
|
||||
for item in bom.get("items", []):
|
||||
qty = item.qty / bom.quantity # quantity per unit
|
||||
qty = item.stock_qty / bom.quantity # quantity per unit
|
||||
exploded_qty = self.exploded_qty * qty
|
||||
if item.bom_no:
|
||||
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)
|
||||
|
@ -6,7 +6,7 @@ from collections import deque
|
||||
from functools import partial
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, timeout
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
@ -27,6 +27,7 @@ test_dependencies = ["Item", "Quality Inspection Template"]
|
||||
|
||||
|
||||
class TestBOM(FrappeTestCase):
|
||||
@timeout
|
||||
def test_get_items(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||
|
||||
@ -37,6 +38,7 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict)
|
||||
self.assertEqual(len(items_dict.values()), 2)
|
||||
|
||||
@timeout
|
||||
def test_get_items_exploded(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict
|
||||
|
||||
@ -49,11 +51,13 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertTrue(test_records[0]["items"][1]["item_code"] in items_dict)
|
||||
self.assertEqual(len(items_dict.values()), 3)
|
||||
|
||||
@timeout
|
||||
def test_get_items_list(self):
|
||||
from erpnext.manufacturing.doctype.bom.bom import get_bom_items
|
||||
|
||||
self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3)
|
||||
|
||||
@timeout
|
||||
def test_default_bom(self):
|
||||
def _get_default_bom_in_item():
|
||||
return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom"))
|
||||
@ -71,6 +75,7 @@ class TestBOM(FrappeTestCase):
|
||||
|
||||
self.assertTrue(_get_default_bom_in_item(), bom.name)
|
||||
|
||||
@timeout
|
||||
def test_update_bom_cost_in_all_boms(self):
|
||||
# get current rate for '_Test Item 2'
|
||||
bom_rates = frappe.db.get_values(
|
||||
@ -99,6 +104,7 @@ class TestBOM(FrappeTestCase):
|
||||
):
|
||||
self.assertEqual(d.base_rate, rm_base_rate + 10)
|
||||
|
||||
@timeout
|
||||
def test_bom_cost(self):
|
||||
bom = frappe.copy_doc(test_records[2])
|
||||
bom.insert()
|
||||
@ -127,6 +133,7 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
||||
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
|
||||
|
||||
@timeout
|
||||
def test_bom_cost_with_batch_size(self):
|
||||
bom = frappe.copy_doc(test_records[2])
|
||||
bom.docstatus = 0
|
||||
@ -145,6 +152,7 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertAlmostEqual(bom.operating_cost, op_cost / 2)
|
||||
bom.delete()
|
||||
|
||||
@timeout
|
||||
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
|
||||
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
|
||||
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):
|
||||
@ -181,6 +189,7 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertEqual(bom.base_raw_material_cost, 27000)
|
||||
self.assertEqual(bom.base_total_cost, 33000)
|
||||
|
||||
@timeout
|
||||
def test_bom_cost_multi_uom_based_on_valuation_rate(self):
|
||||
bom = frappe.copy_doc(test_records[2])
|
||||
bom.set_rate_of_sub_assembly_item_based_on_bom = 0
|
||||
@ -202,6 +211,7 @@ class TestBOM(FrappeTestCase):
|
||||
|
||||
self.assertEqual(bom.items[0].rate, 20)
|
||||
|
||||
@timeout
|
||||
def test_bom_cost_with_fg_based_operating_cost(self):
|
||||
bom = frappe.copy_doc(test_records[4])
|
||||
bom.insert()
|
||||
@ -229,6 +239,7 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
|
||||
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
|
||||
|
||||
@timeout
|
||||
def test_subcontractor_sourced_item(self):
|
||||
item_code = "_Test Subcontracted FG Item 1"
|
||||
set_backflush_based_on("Material Transferred for Subcontract")
|
||||
@ -310,6 +321,7 @@ class TestBOM(FrappeTestCase):
|
||||
supplied_items = sorted([d.rm_item_code for d in sco.supplied_items])
|
||||
self.assertEqual(bom_items, supplied_items)
|
||||
|
||||
@timeout
|
||||
def test_bom_tree_representation(self):
|
||||
bom_tree = {
|
||||
"Assembly": {
|
||||
@ -335,6 +347,7 @@ class TestBOM(FrappeTestCase):
|
||||
for reqd_item, created_item in zip(reqd_order, created_order):
|
||||
self.assertEqual(reqd_item, created_item.item_code)
|
||||
|
||||
@timeout
|
||||
def test_generated_variant_bom(self):
|
||||
from erpnext.controllers.item_variant import create_variant
|
||||
|
||||
@ -375,6 +388,7 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertEqual(reqd_item.qty, created_item.qty)
|
||||
self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
|
||||
|
||||
@timeout
|
||||
def test_bom_recursion_1st_level(self):
|
||||
"""BOM should not allow BOM item again in child"""
|
||||
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||
@ -387,6 +401,7 @@ class TestBOM(FrappeTestCase):
|
||||
bom.items[0].bom_no = bom.name
|
||||
bom.save()
|
||||
|
||||
@timeout
|
||||
def test_bom_recursion_transitive(self):
|
||||
item1 = make_item(properties={"is_stock_item": 1}).name
|
||||
item2 = make_item(properties={"is_stock_item": 1}).name
|
||||
@ -408,6 +423,7 @@ class TestBOM(FrappeTestCase):
|
||||
bom1.save()
|
||||
bom2.save()
|
||||
|
||||
@timeout
|
||||
def test_bom_with_process_loss_item(self):
|
||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||
|
||||
@ -421,6 +437,7 @@ class TestBOM(FrappeTestCase):
|
||||
# Items with whole UOMs can't be PL Items
|
||||
self.assertRaises(frappe.ValidationError, bom_doc.submit)
|
||||
|
||||
@timeout
|
||||
def test_bom_item_query(self):
|
||||
query = partial(
|
||||
item_query,
|
||||
@ -440,6 +457,7 @@ class TestBOM(FrappeTestCase):
|
||||
)
|
||||
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
|
||||
|
||||
@timeout
|
||||
def test_exclude_exploded_items_from_bom(self):
|
||||
bom_no = get_default_bom()
|
||||
new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no))
|
||||
@ -458,6 +476,7 @@ class TestBOM(FrappeTestCase):
|
||||
|
||||
new_bom.delete()
|
||||
|
||||
@timeout
|
||||
def test_valid_transfer_defaults(self):
|
||||
bom_with_op = frappe.db.get_value(
|
||||
"BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1}
|
||||
@ -489,11 +508,13 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertEqual(bom.transfer_material_against, "Work Order")
|
||||
bom.delete()
|
||||
|
||||
@timeout
|
||||
def test_bom_name_length(self):
|
||||
"""test >140 char names"""
|
||||
bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}}
|
||||
create_nested_bom(bom_tree, prefix="")
|
||||
|
||||
@timeout
|
||||
def test_version_index(self):
|
||||
|
||||
bom = frappe.new_doc("BOM")
|
||||
@ -515,6 +536,7 @@ class TestBOM(FrappeTestCase):
|
||||
msg=f"Incorrect index for {existing_boms}",
|
||||
)
|
||||
|
||||
@timeout
|
||||
def test_bom_versioning(self):
|
||||
bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}}
|
||||
bom = create_nested_bom(bom_tree, prefix="")
|
||||
@ -547,6 +569,7 @@ class TestBOM(FrappeTestCase):
|
||||
self.assertNotEqual(amendment.name, version.name)
|
||||
self.assertEqual(int(version.name.split("-")[-1]), 2)
|
||||
|
||||
@timeout
|
||||
def test_clear_inpection_quality(self):
|
||||
|
||||
bom = frappe.copy_doc(test_records[2], ignore_no_copy=True)
|
||||
@ -565,6 +588,7 @@ class TestBOM(FrappeTestCase):
|
||||
|
||||
self.assertEqual(bom.quality_inspection_template, None)
|
||||
|
||||
@timeout
|
||||
def test_bom_pricing_based_on_lpp(self):
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
|
||||
@ -585,6 +609,7 @@ class TestBOM(FrappeTestCase):
|
||||
bom.submit()
|
||||
self.assertEqual(bom.items[0].rate, 42)
|
||||
|
||||
@timeout
|
||||
def test_set_default_bom_for_item_having_single_bom(self):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
@ -621,6 +646,7 @@ class TestBOM(FrappeTestCase):
|
||||
bom.reload()
|
||||
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
|
||||
|
||||
@timeout
|
||||
def test_exploded_items_rate(self):
|
||||
rm_item = make_item(
|
||||
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
||||
@ -649,6 +675,7 @@ class TestBOM(FrappeTestCase):
|
||||
bom.submit()
|
||||
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
|
||||
|
||||
@timeout
|
||||
def test_bom_cost_update_flag(self):
|
||||
rm_item = make_item(
|
||||
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
|
||||
|
@ -2,7 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.tests.utils import FrappeTestCase, timeout
|
||||
|
||||
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
||||
update_cost_in_all_boms_in_test,
|
||||
@ -20,6 +20,7 @@ class TestBOMUpdateTool(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
@timeout
|
||||
def test_replace_bom(self):
|
||||
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
||||
|
||||
@ -33,6 +34,7 @@ class TestBOMUpdateTool(FrappeTestCase):
|
||||
self.assertFalse(frappe.db.exists("BOM Item", {"bom_no": current_bom, "docstatus": 1}))
|
||||
self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
|
||||
|
||||
@timeout
|
||||
def test_bom_cost(self):
|
||||
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]:
|
||||
item_doc = create_item(item, valuation_rate=100)
|
||||
|
@ -682,7 +682,7 @@ class WorkOrder(Document):
|
||||
|
||||
for node in bom_traversal:
|
||||
if node.is_bom:
|
||||
operations.extend(_get_operations(node.name, qty=node.exploded_qty))
|
||||
operations.extend(_get_operations(node.name, qty=node.exploded_qty / node.bom_qty))
|
||||
|
||||
bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
|
||||
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))
|
||||
|
@ -322,8 +322,10 @@ erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
|
||||
erpnext.patches.v14_0.update_entry_type_for_journal_entry
|
||||
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
|
||||
erpnext.patches.v14_0.set_pick_list_status
|
||||
erpnext.patches.v13_0.update_docs_link
|
||||
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
|
||||
erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
|
||||
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
|
||||
erpnext.patches.v14_0.update_closing_balances
|
||||
# below 2 migration patches should always run last
|
||||
# below migration patches should always run last
|
||||
erpnext.patches.v14_0.migrate_gl_to_payment_ledger
|
||||
|
14
erpnext/patches/v13_0/update_docs_link.py
Normal file
14
erpnext/patches/v13_0/update_docs_link.py
Normal file
@ -0,0 +1,14 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
navbar_settings = frappe.get_single("Navbar Settings")
|
||||
for item in navbar_settings.help_dropdown:
|
||||
if item.is_standard and item.route == "https://erpnext.com/docs/user/manual":
|
||||
item.route = "https://docs.erpnext.com/docs/v14/user/manual/en/introduction"
|
||||
|
||||
navbar_settings.save()
|
20
erpnext/patches/v15_0/update_gpa_and_ndb_for_assdeprsch.py
Normal file
20
erpnext/patches/v15_0/update_gpa_and_ndb_for_assdeprsch.py
Normal file
@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
# not using frappe.qb because https://github.com/frappe/frappe/issues/20292
|
||||
frappe.db.sql(
|
||||
"""UPDATE `tabAsset Depreciation Schedule`
|
||||
JOIN `tabAsset`
|
||||
ON `tabAsset Depreciation Schedule`.`asset`=`tabAsset`.`name`
|
||||
SET
|
||||
`tabAsset Depreciation Schedule`.`gross_purchase_amount`=`tabAsset`.`gross_purchase_amount`,
|
||||
`tabAsset Depreciation Schedule`.`number_of_depreciations_booked`=`tabAsset`.`number_of_depreciations_booked`
|
||||
WHERE
|
||||
(
|
||||
`tabAsset Depreciation Schedule`.`gross_purchase_amount`<>`tabAsset`.`gross_purchase_amount`
|
||||
OR
|
||||
`tabAsset Depreciation Schedule`.`number_of_depreciations_booked`<>`tabAsset`.`number_of_depreciations_booked`
|
||||
)
|
||||
AND `tabAsset Depreciation Schedule`.`docstatus`<2"""
|
||||
)
|
@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.manufacturing.doctype.blanket_order.blanket_order import (
|
||||
validate_against_blanket_order,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.production_plan.production_plan import (
|
||||
get_items_for_material_requests,
|
||||
)
|
||||
@ -52,6 +55,7 @@ class SalesOrder(SellingController):
|
||||
self.validate_warehouse()
|
||||
self.validate_drop_ship()
|
||||
self.validate_serial_no_based_delivery()
|
||||
validate_against_blanket_order(self)
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.customer, self.company, self.inter_company_order_reference
|
||||
)
|
||||
|
@ -24,6 +24,7 @@
|
||||
"so_required",
|
||||
"dn_required",
|
||||
"sales_update_frequency",
|
||||
"over_order_allowance",
|
||||
"column_break_5",
|
||||
"allow_multiple_items",
|
||||
"allow_against_multiple_purchase_orders",
|
||||
@ -179,6 +180,12 @@
|
||||
"fieldname": "allow_sales_order_creation_for_expired_quotation",
|
||||
"fieldtype": "Check",
|
||||
"label": "Allow Sales Order Creation For Expired Quotation"
|
||||
},
|
||||
{
|
||||
"description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.",
|
||||
"fieldname": "over_order_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Order Allowance (%)"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
@ -186,7 +193,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2023-02-04 12:37:53.380857",
|
||||
"modified": "2023-03-03 11:16:54.333615",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
|
@ -808,7 +808,7 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad
|
||||
return existing_address
|
||||
|
||||
if out:
|
||||
return min(out, key=lambda x: x[1])[0] # find min by sort_key
|
||||
return max(out, key=lambda x: x[1])[0] # find max by sort_key
|
||||
else:
|
||||
return None
|
||||
|
||||
|
@ -11,6 +11,7 @@ from frappe.utils import random_string
|
||||
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
|
||||
get_charts_for_country,
|
||||
)
|
||||
from erpnext.setup.doctype.company.company import get_default_company_address
|
||||
|
||||
test_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"]
|
||||
test_dependencies = ["Fiscal Year"]
|
||||
@ -132,6 +133,38 @@ class TestCompany(unittest.TestCase):
|
||||
self.assertTrue(lft >= min_lft)
|
||||
self.assertTrue(rgt <= max_rgt)
|
||||
|
||||
def test_primary_address(self):
|
||||
company = "_Test Company"
|
||||
|
||||
secondary = frappe.get_doc(
|
||||
{
|
||||
"address_title": "Non Primary",
|
||||
"doctype": "Address",
|
||||
"address_type": "Billing",
|
||||
"address_line1": "Something",
|
||||
"city": "Mumbai",
|
||||
"state": "Maharashtra",
|
||||
"country": "India",
|
||||
"is_primary_address": 1,
|
||||
"pincode": "400098",
|
||||
"links": [
|
||||
{
|
||||
"link_doctype": "Company",
|
||||
"link_name": company,
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
secondary.insert()
|
||||
self.addCleanup(secondary.delete)
|
||||
|
||||
primary = frappe.copy_doc(secondary)
|
||||
primary.is_primary_address = 1
|
||||
primary.insert()
|
||||
self.addCleanup(primary.delete)
|
||||
|
||||
self.assertEqual(get_default_company_address(company), primary.name)
|
||||
|
||||
def get_no_of_children(self, company):
|
||||
def get_no_of_children(companies, no_of_children):
|
||||
children = []
|
||||
|
@ -155,7 +155,7 @@ def add_standard_navbar_items():
|
||||
{
|
||||
"item_label": "Documentation",
|
||||
"item_type": "Route",
|
||||
"route": "https://erpnext.com/docs/user/manual",
|
||||
"route": "https://docs.erpnext.com/docs/v14/user/manual/en/introduction",
|
||||
"is_standard": 1,
|
||||
},
|
||||
{
|
||||
|
@ -377,7 +377,9 @@ class Item(Document):
|
||||
"" if item_barcode.barcode_type not in options else item_barcode.barcode_type
|
||||
)
|
||||
if item_barcode.barcode_type:
|
||||
barcode_type = convert_erpnext_to_barcodenumber(item_barcode.barcode_type.upper())
|
||||
barcode_type = convert_erpnext_to_barcodenumber(
|
||||
item_barcode.barcode_type.upper(), item_barcode.barcode
|
||||
)
|
||||
if barcode_type in barcodenumber.barcodes():
|
||||
if not barcodenumber.check_code(barcode_type, item_barcode.barcode):
|
||||
frappe.throw(
|
||||
@ -982,20 +984,29 @@ class Item(Document):
|
||||
)
|
||||
|
||||
|
||||
def convert_erpnext_to_barcodenumber(erpnext_number):
|
||||
def convert_erpnext_to_barcodenumber(erpnext_number, barcode):
|
||||
if erpnext_number == "EAN":
|
||||
ean_type = {
|
||||
8: "EAN8",
|
||||
13: "EAN13",
|
||||
}
|
||||
barcode_length = len(barcode)
|
||||
if barcode_length in ean_type:
|
||||
return ean_type[barcode_length]
|
||||
|
||||
return erpnext_number
|
||||
|
||||
convert = {
|
||||
"UPC-A": "UPCA",
|
||||
"CODE-39": "CODE39",
|
||||
"EAN": "EAN13",
|
||||
"EAN-12": "EAN",
|
||||
"EAN-8": "EAN8",
|
||||
"ISBN-10": "ISBN10",
|
||||
"ISBN-13": "ISBN13",
|
||||
}
|
||||
|
||||
if erpnext_number in convert:
|
||||
return convert[erpnext_number]
|
||||
else:
|
||||
return erpnext_number
|
||||
|
||||
return erpnext_number
|
||||
|
||||
|
||||
def make_item_price(item, price_list_name, item_price):
|
||||
|
@ -581,8 +581,9 @@ class TestItem(FrappeTestCase):
|
||||
},
|
||||
{"barcode": "72527273070", "barcode_type": "UPC-A"},
|
||||
{"barcode": "123456", "barcode_type": "CODE-39"},
|
||||
{"barcode": "401268452363", "barcode_type": "EAN-12"},
|
||||
{"barcode": "90311017", "barcode_type": "EAN-8"},
|
||||
{"barcode": "401268452363", "barcode_type": "EAN"},
|
||||
{"barcode": "90311017", "barcode_type": "EAN"},
|
||||
{"barcode": "73513537", "barcode_type": "EAN"},
|
||||
{"barcode": "0123456789012", "barcode_type": "GS1"},
|
||||
{"barcode": "2211564566668", "barcode_type": "GTIN"},
|
||||
{"barcode": "0256480249", "barcode_type": "ISBN"},
|
||||
|
@ -172,8 +172,8 @@ class PickList(Document):
|
||||
if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance:
|
||||
frappe.throw(
|
||||
_(
|
||||
f"You are picking more than required quantity for the item {row.item_code}. Check if there is any other pick list created for the sales order {row.sales_order}."
|
||||
)
|
||||
"You are picking more than required quantity for the item {0}. Check if there is any other pick list created for the sales order {1}."
|
||||
).format(row.item_code, row.sales_order)
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -658,6 +658,7 @@ class StockEntry(StockController):
|
||||
)
|
||||
finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
|
||||
|
||||
items = []
|
||||
# Set basic rate for incoming items
|
||||
for d in self.get("items"):
|
||||
if d.s_warehouse or d.set_basic_rate_manually:
|
||||
@ -665,12 +666,7 @@ class StockEntry(StockController):
|
||||
|
||||
if d.allow_zero_valuation_rate:
|
||||
d.basic_rate = 0.0
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Row {0}: Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {1}"
|
||||
).format(d.idx, d.item_code),
|
||||
alert=1,
|
||||
)
|
||||
items.append(d.item_code)
|
||||
|
||||
elif d.is_finished_item:
|
||||
if self.purpose == "Manufacture":
|
||||
@ -697,6 +693,20 @@ class StockEntry(StockController):
|
||||
d.basic_rate = flt(d.basic_rate)
|
||||
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
|
||||
|
||||
if items:
|
||||
message = ""
|
||||
|
||||
if len(items) > 1:
|
||||
message = _(
|
||||
"Items rate has been updated to zero as Allow Zero Valuation Rate is checked for the following items: {0}"
|
||||
).format(", ".join(frappe.bold(item) for item in items))
|
||||
else:
|
||||
message = _(
|
||||
"Item rate has been updated to zero as Allow Zero Valuation Rate is checked for item {0}"
|
||||
).format(frappe.bold(items[0]))
|
||||
|
||||
frappe.msgprint(message, alert=True)
|
||||
|
||||
def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
|
||||
outgoing_items_cost = 0.0
|
||||
for d in self.get("items"):
|
||||
|
Loading…
x
Reference in New Issue
Block a user