Merge branch 'develop' into early-payment-loss

This commit is contained in:
Marica 2023-03-21 11:40:30 +05:30 committed by GitHub
commit c5da7f5fb1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
46 changed files with 569 additions and 174 deletions

View File

@ -8,8 +8,9 @@ sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"} 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 git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench 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/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_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}" bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi

View File

@ -394,7 +394,13 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if ancestors and not allow_independent_account_creation: if ancestors and not allow_independent_account_creation:
for ancestor in ancestors: 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 # same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company") allow_child_account_creation = _("Allow Account Creation Against Child Company")

View File

@ -221,12 +221,15 @@ class PaymentReconciliation(Document):
def get_difference_amount(self, payment_entry, invoice, allocated_amount): def get_difference_amount(self, payment_entry, invoice, allocated_amount):
difference_amount = 0 difference_amount = 0
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( if frappe.get_cached_value(
"exchange_rate", 1 "Account", self.receivable_payable_account, "account_currency"
): ) != frappe.get_cached_value("Company", self.company, "default_currency"):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount "exchange_rate", 1
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate ):
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 return difference_amount

View File

@ -5,7 +5,7 @@ import unittest
import frappe import frappe
from frappe import qb 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 frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center from erpnext import get_default_cost_center
@ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": 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() pr.reconcile()
si.reload() si.reload()
@ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": 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() pr.reconcile()
# check PR tool output # check PR tool output
@ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": 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() pr.reconcile()
# assert outstanding # assert outstanding
@ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": 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() pr.reconcile()
self.assertEqual(pr.get("invoices"), []) self.assertEqual(pr.get("invoices"), [])
@ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase):
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name]) 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): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):

View File

@ -15,7 +15,7 @@
</div> </div>
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2> <h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
<div> <div>
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5> <h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party_name[0] }}</b></h5>
<h5 style="float: right;"> <h5 style="float: right;">
{{ _("Date: ") }} {{ _("Date: ") }}
<b>{{ frappe.format(filters.from_date, 'Date')}} <b>{{ frappe.format(filters.from_date, 'Date')}}

View File

@ -24,7 +24,7 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document): class ProcessStatementOfAccounts(Document):
def validate(self): def validate(self):
if not self.subject: if not self.subject:
self.subject = "Statement Of Accounts for {{ customer.name }}" self.subject = "Statement Of Accounts for {{ customer.customer_name }}"
if not self.body: if not self.body:
self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." self.body = "Hello {{ customer.name }},<br>PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}."
@ -87,6 +87,7 @@ def get_report_pdf(doc, consolidated=True):
"account": [doc.account] if doc.account else None, "account": [doc.account] if doc.account else None,
"party_type": "Customer", "party_type": "Customer",
"party": [entry.customer], "party": [entry.customer],
"party_name": [entry.customer_name] if entry.customer_name else None,
"presentation_currency": presentation_currency, "presentation_currency": presentation_currency,
"group_by": doc.group_by, "group_by": doc.group_by,
"currency": doc.currency, "currency": doc.currency,
@ -156,7 +157,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
] ]
return frappe.get_list( return frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]], filters=[[fields_dict[customer_collection], "IN", selected]],
) )
@ -179,7 +180,7 @@ def get_customers_based_on_sales_person(sales_person):
if sales_person_records.get("Customer"): if sales_person_records.get("Customer"):
return frappe.get_list( return frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[["name", "in", list(sales_person_records["Customer"])]], filters=[["name", "in", list(sales_person_records["Customer"])]],
) )
else: else:
@ -228,7 +229,7 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
if customer_collection == "Sales Partner": if customer_collection == "Sales Partner":
customers = frappe.get_list( customers = frappe.get_list(
"Customer", "Customer",
fields=["name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[["default_sales_partner", "=", collection_name]], filters=[["default_sales_partner", "=", collection_name]],
) )
else: else:
@ -245,7 +246,12 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
continue continue
customer_list.append( customer_list.append(
{"name": customer.name, "primary_email": primary_email, "billing_email": billing_email} {
"name": customer.name,
"customer_name": customer.customer_name,
"primary_email": primary_email,
"billing_email": billing_email,
}
) )
return customer_list return customer_list

View File

@ -1,12 +1,12 @@
{ {
"actions": [], "actions": [],
"allow_workflow": 1,
"creation": "2020-08-03 16:35:21.852178", "creation": "2020-08-03 16:35:21.852178",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"customer", "customer",
"customer_name",
"billing_email", "billing_email",
"primary_email" "primary_email"
], ],
@ -30,11 +30,18 @@
"fieldtype": "Read Only", "fieldtype": "Read Only",
"in_list_view": 1, "in_list_view": 1,
"label": "Billing Email" "label": "Billing Email"
},
{
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"label": "Customer Name",
"read_only": 1
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2020-08-03 22:55:38.875601", "modified": "2023-03-13 00:12:34.508086",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Process Statement Of Accounts Customer", "name": "Process Statement Of Accounts Customer",
@ -43,5 +50,6 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@ -32,9 +32,6 @@
"cost_center", "cost_center",
"dimension_col_break", "dimension_col_break",
"project", "project",
"column_break_27",
"campaign",
"source",
"currency_and_price_list", "currency_and_price_list",
"currency", "currency",
"conversion_rate", "conversion_rate",
@ -203,7 +200,9 @@
"more_information", "more_information",
"status", "status",
"inter_company_invoice_reference", "inter_company_invoice_reference",
"campaign",
"represents_company", "represents_company",
"source",
"customer_group", "customer_group",
"col_break23", "col_break23",
"is_internal_customer", "is_internal_customer",
@ -2083,10 +2082,6 @@
"fieldname": "company_addr_col_break", "fieldname": "company_addr_col_break",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "column_break_27",
"fieldtype": "Column Break"
},
{ {
"fieldname": "column_break_52", "fieldname": "column_break_52",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -2143,11 +2138,10 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-01-28 19:45:47.538163", "modified": "2023-03-13 11:43:15.883055",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -266,16 +266,16 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Education Cess - _TC": [3, 1618, 0.06, 32.36], "_Test Account Education Cess - _TC": [3, 1618, 0.06, 32.36],
"_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39], "_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39],
"_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04], "_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04],
"_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17], "_Test Account VAT - _TC": [156.0, 1808.0, 3.12, 36.16],
"_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55], "_Test Account Discount - _TC": [-181.0, 1627.0, -3.62, 32.54],
} }
for d in si.get("taxes"): for d in si.get("taxes"):
for i, k in enumerate(expected_values["keys"]): for i, k in enumerate(expected_values["keys"]):
self.assertEqual(d.get(k), expected_values[d.account_head][i]) self.assertEqual(d.get(k), expected_values[d.account_head][i])
self.assertEqual(si.base_grand_total, 1627.5) self.assertEqual(si.base_grand_total, 1627.0)
self.assertEqual(si.grand_total, 32.55) self.assertEqual(si.grand_total, 32.54)
def test_sales_invoice_with_discount_and_inclusive_tax(self): def test_sales_invoice_with_discount_and_inclusive_tax(self):
si = create_sales_invoice(qty=100, rate=50, do_not_save=True) si = create_sales_invoice(qty=100, rate=50, do_not_save=True)
@ -401,10 +401,10 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account S&H Education Cess - _TC": [1.4, 1.30, 1297.67], "_Test Account S&H Education Cess - _TC": [1.4, 1.30, 1297.67],
"_Test Account CST - _TC": [27.88, 25.95, 1323.62], "_Test Account CST - _TC": [27.88, 25.95, 1323.62],
"_Test Account VAT - _TC": [156.25, 145.43, 1469.05], "_Test Account VAT - _TC": [156.25, 145.43, 1469.05],
"_Test Account Customs Duty - _TC": [125, 116.35, 1585.40], "_Test Account Customs Duty - _TC": [125, 116.34, 1585.39],
"_Test Account Shipping Charges - _TC": [100, 100, 1685.40], "_Test Account Shipping Charges - _TC": [100, 100, 1685.39],
"_Test Account Discount - _TC": [-180.33, -168.54, 1516.86], "_Test Account Discount - _TC": [-180.33, -168.54, 1516.85],
"_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01], "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.00],
} }
for d in si.get("taxes"): for d in si.get("taxes"):
@ -413,7 +413,7 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(si.base_grand_total, 1500) self.assertEqual(si.base_grand_total, 1500)
self.assertEqual(si.grand_total, 1500) self.assertEqual(si.grand_total, 1500)
self.assertEqual(si.rounding_adjustment, -0.01) self.assertEqual(si.rounding_adjustment, 0.0)
def test_discount_amount_gl_entry(self): def test_discount_amount_gl_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
@ -454,7 +454,7 @@ class TestSalesInvoice(unittest.TestCase):
[test_records[3]["taxes"][2]["account_head"], 0.0, 1.30], [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30],
[test_records[3]["taxes"][3]["account_head"], 0.0, 25.95], [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95],
[test_records[3]["taxes"][4]["account_head"], 0.0, 145.43], [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43],
[test_records[3]["taxes"][5]["account_head"], 0.0, 116.35], [test_records[3]["taxes"][5]["account_head"], 0.0, 116.34],
[test_records[3]["taxes"][6]["account_head"], 0.0, 100], [test_records[3]["taxes"][6]["account_head"], 0.0, 100],
[test_records[3]["taxes"][7]["account_head"], 168.54, 0.0], [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0],
["_Test Account Service Tax - _TC", 16.85, 0.0], ["_Test Account Service Tax - _TC", 16.85, 0.0],
@ -1614,7 +1614,7 @@ class TestSalesInvoice(unittest.TestCase):
"_Test Account Education Cess - _TC": [1.4, 1.4, 1.4], "_Test Account Education Cess - _TC": [1.4, 1.4, 1.4],
"_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7], "_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7],
"_Test Account CST - _TC": [17.19, 17.19, 17.19], "_Test Account CST - _TC": [17.19, 17.19, 17.19],
"_Test Account VAT - _TC": [78.13, 78.13, 78.13], "_Test Account VAT - _TC": [78.12, 78.12, 78.12],
"_Test Account Discount - _TC": [-95.49, -95.49, -95.49], "_Test Account Discount - _TC": [-95.49, -95.49, -95.49],
} }
@ -1623,9 +1623,9 @@ class TestSalesInvoice(unittest.TestCase):
if expected_values.get(d.account_head): if expected_values.get(d.account_head):
self.assertEqual(d.get(k), expected_values[d.account_head][i]) self.assertEqual(d.get(k), expected_values[d.account_head][i])
self.assertEqual(si.total_taxes_and_charges, 234.43) self.assertEqual(si.total_taxes_and_charges, 234.42)
self.assertEqual(si.base_grand_total, 859.43) self.assertEqual(si.base_grand_total, 859.42)
self.assertEqual(si.grand_total, 859.43) self.assertEqual(si.grand_total, 859.42)
def test_multi_currency_gle(self): def test_multi_currency_gle(self):
si = create_sales_invoice( si = create_sales_invoice(
@ -1985,17 +1985,17 @@ class TestSalesInvoice(unittest.TestCase):
) )
si.save() si.save()
si.submit() si.submit()
self.assertEqual(si.net_total, 19453.13) self.assertEqual(si.net_total, 19453.12)
self.assertEqual(si.grand_total, 24900) self.assertEqual(si.grand_total, 24900)
self.assertEqual(si.total_taxes_and_charges, 5446.88) self.assertEqual(si.total_taxes_and_charges, 5446.88)
self.assertEqual(si.rounding_adjustment, -0.01) self.assertEqual(si.rounding_adjustment, 0.0)
expected_values = dict( expected_values = dict(
(d[0], d) (d[0], d)
for d in [ for d in [
[si.debit_to, 24900, 0.0], [si.debit_to, 24900, 0.0],
["_Test Account Service Tax - _TC", 0.0, 5446.88], ["_Test Account Service Tax - _TC", 0.0, 5446.88],
["Sales - _TC", 0.0, 19453.13], ["Sales - _TC", 0.0, 19453.12],
["Round Off - _TC", 0.01, 0.0], ["Round Off - _TC", 0.01, 0.0],
] ]
) )

View File

@ -32,6 +32,16 @@ from erpnext import get_company_currency
from erpnext.accounts.utils import get_fiscal_year from erpnext.accounts.utils import get_fiscal_year
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
PURCHASE_TRANSACTION_TYPES = {"Purchase Order", "Purchase Receipt", "Purchase Invoice"}
SALES_TRANSACTION_TYPES = {
"Quotation",
"Sales Order",
"Delivery Note",
"Sales Invoice",
"POS Invoice",
}
TRANSACTION_TYPES = PURCHASE_TRANSACTION_TYPES | SALES_TRANSACTION_TYPES
class DuplicatePartyAccountError(frappe.ValidationError): class DuplicatePartyAccountError(frappe.ValidationError):
pass pass
@ -124,12 +134,6 @@ def _get_party_details(
set_other_values(party_details, party, party_type) set_other_values(party_details, party, party_type)
set_price_list(party_details, party, party_type, price_list, pos_profile) set_price_list(party_details, party, party_type, price_list, pos_profile)
party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"),
party_address,
shipping_address if party_type != "Supplier" else party_address,
)
tax_template = set_taxes( tax_template = set_taxes(
party.name, party.name,
party_type, party_type,
@ -211,20 +215,10 @@ def set_address_details(
else: else:
party_details.update(get_company_address(company)) party_details.update(get_company_address(company))
if doctype and doctype in [ if doctype in SALES_TRANSACTION_TYPES and party_details.company_address:
"Delivery Note", party_details.update(get_fetch_values(doctype, "company_address", party_details.company_address))
"Sales Invoice",
"Sales Order",
"Quotation",
"POS Invoice",
]:
if party_details.company_address:
party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address)
)
get_regional_address_details(party_details, doctype, company)
elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: if doctype in PURCHASE_TRANSACTION_TYPES:
if shipping_address: if shipping_address:
party_details.update( party_details.update(
shipping_address=shipping_address, shipping_address=shipping_address,
@ -250,9 +244,21 @@ def set_address_details(
**get_fetch_values(doctype, "shipping_address", party_details.billing_address) **get_fetch_values(doctype, "shipping_address", party_details.billing_address)
) )
party_address, shipping_address = (
party_details.get(billing_address_field),
party_details.shipping_address_name,
)
party_details["tax_category"] = get_address_tax_category(
party.get("tax_category"),
party_address,
shipping_address if party_type != "Supplier" else party_address,
)
if doctype in TRANSACTION_TYPES:
get_regional_address_details(party_details, doctype, company) get_regional_address_details(party_details, doctype, company)
return party_details.get(billing_address_field), party_details.shipping_address_name return party_address, shipping_address
@erpnext.allow_regional @erpnext.allow_regional

View File

@ -859,7 +859,7 @@ class ReceivablePayableReport(object):
) )
else: else:
self.qb_selection_filter.append( 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): def is_invoice(self, ple):

View File

@ -501,7 +501,14 @@ class GrossProfitGenerator(object):
): ):
returned_item_rows = self.returned_invoices[row.parent][row.item_code] returned_item_rows = self.returned_invoices[row.parent][row.item_code]
for returned_item_row in returned_item_rows: 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.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) row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
if flt(row.qty) or row.base_amount: if flt(row.qty) or row.base_amount:
@ -734,6 +741,8 @@ class GrossProfitGenerator(object):
if self.filters.to_date: if self.filters.to_date:
conditions += " and posting_date <= %(to_date)s" 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: if self.filters.item_group:
conditions += " and {0}".format(get_item_group_condition(self.filters.item_group)) conditions += " and {0}".format(get_item_group_condition(self.filters.item_group))

View File

@ -381,3 +381,82 @@ class TestGrossProfit(FrappeTestCase):
} }
gp_entry = [x for x in data if x.parent_invoice == sinv.name] gp_entry = [x for x in data if x.parent_invoice == sinv.name]
self.assertDictContainsSubset(expected_entry, gp_entry[0]) 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])

View File

@ -78,7 +78,6 @@ def validate_filters(filters):
def get_data(filters): def get_data(filters):
accounts = frappe.db.sql( accounts = frappe.db.sql(
"""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt """select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt
@ -118,12 +117,10 @@ def get_data(filters):
ignore_closing_entries=not flt(filters.with_period_closing_entry), ignore_closing_entries=not flt(filters.with_period_closing_entry),
) )
total_row = calculate_values( calculate_values(accounts, gl_entries_by_account, opening_balances)
accounts, gl_entries_by_account, opening_balances, filters, company_currency
)
accumulate_values_into_parents(accounts, accounts_by_name) 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 = filter_out_zero_value_rows(
data, parent_children_map, show_zero_values=filters.get("show_zero_values") data, parent_children_map, show_zero_values=filters.get("show_zero_values")
) )
@ -218,7 +215,7 @@ def get_rootwise_opening_balances(filters, report_type):
return opening return opening
def calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency): def calculate_values(accounts, gl_entries_by_account, opening_balances):
init = { init = {
"opening_debit": 0.0, "opening_debit": 0.0,
"opening_credit": 0.0, "opening_credit": 0.0,
@ -228,22 +225,6 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
"closing_credit": 0.0, "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: for d in accounts:
d.update(init.copy()) d.update(init.copy())
@ -261,8 +242,28 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, filters,
prepare_opening_closing(d) 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 return total_row
@ -274,7 +275,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name):
accounts_by_name[d.parent_account][key] += d[key] 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 = [] data = []
for d in accounts: for d in accounts:
@ -305,6 +306,7 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr
row["has_value"] = has_value row["has_value"] = has_value
data.append(row) data.append(row)
total_row = calculate_total_row(accounts, company_currency)
data.extend([{}, total_row]) data.extend([{}, total_row])
return data return data

View File

@ -455,7 +455,9 @@ def reconcile_against_document(args): # nosemgrep
try: try:
doc.validate_total_debit_and_credit() doc.validate_total_debit_and_credit()
except Exception as validation_exception: 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) doc.save(ignore_permissions=True)
# re-submit advance entry # re-submit advance entry

View File

@ -828,8 +828,8 @@ class TestDepreciationMethods(AssetSetup):
expected_schedules = [ expected_schedules = [
["2030-12-31", 28630.14, 28630.14], ["2030-12-31", 28630.14, 28630.14],
["2031-12-31", 35684.93, 64315.07], ["2031-12-31", 35684.93, 64315.07],
["2032-12-31", 17842.47, 82157.54], ["2032-12-31", 17842.46, 82157.53],
["2033-06-06", 5342.46, 87500.0], ["2033-06-06", 5342.47, 87500.0],
] ]
schedules = [ schedules = [

View File

@ -140,8 +140,8 @@ class AssetDepreciationSchedule(Document):
self.asset = asset_doc.name self.asset = asset_doc.name
self.finance_book = row.finance_book self.finance_book = row.finance_book
self.finance_book_id = row.idx self.finance_book_id = row.idx
self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation self.opening_accumulated_depreciation = asset_doc.opening_accumulated_depreciation or 0
self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked self.number_of_depreciations_booked = asset_doc.number_of_depreciations_booked or 0
self.gross_purchase_amount = asset_doc.gross_purchase_amount self.gross_purchase_amount = asset_doc.gross_purchase_amount
self.depreciation_method = row.depreciation_method self.depreciation_method = row.depreciation_method
self.total_number_of_depreciations = row.total_number_of_depreciations self.total_number_of_depreciations = row.total_number_of_depreciations
@ -185,14 +185,14 @@ class AssetDepreciationSchedule(Document):
): ):
asset_doc.validate_asset_finance_books(row) 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 row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row: if update_asset_finance_book_row:
row.db_update() row.db_update()
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( 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) has_pro_rata = asset_doc.check_is_pro_rata(row)
@ -235,13 +235,12 @@ class AssetDepreciationSchedule(Document):
self.add_depr_schedule_row( self.add_depr_schedule_row(
date_of_disposal, date_of_disposal,
depreciation_amount, depreciation_amount,
row.depreciation_method,
) )
break break
# For first row # 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( from_date = add_days(
asset_doc.available_for_use_date, -1 asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too ) # 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 # 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.to_date = add_months(
asset_doc.available_for_use_date, 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 depreciation_amount_without_pro_rata = depreciation_amount
@ -298,7 +297,6 @@ class AssetDepreciationSchedule(Document):
self.add_depr_schedule_row( self.add_depr_schedule_row(
schedule_date, schedule_date,
depreciation_amount, depreciation_amount,
row.depreciation_method,
) )
# to ensure that final accumulated depreciation amount is accurate # to ensure that final accumulated depreciation amount is accurate
@ -325,14 +323,12 @@ class AssetDepreciationSchedule(Document):
self, self,
schedule_date, schedule_date,
depreciation_amount, depreciation_amount,
depreciation_method,
): ):
self.append( self.append(
"depreciation_schedule", "depreciation_schedule",
{ {
"schedule_date": schedule_date, "schedule_date": schedule_date,
"depreciation_amount": depreciation_amount, "depreciation_amount": depreciation_amount,
"depreciation_method": depreciation_method,
}, },
) )
@ -346,7 +342,7 @@ class AssetDepreciationSchedule(Document):
straight_line_idx = [ straight_line_idx = [
d.idx d.idx
for d in self.get("depreciation_schedule") 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) accumulated_depreciation = flt(self.opening_accumulated_depreciation)
@ -377,16 +373,15 @@ class AssetDepreciationSchedule(Document):
accumulated_depreciation, d.precision("accumulated_depreciation_amount") 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): return value_after_depreciation
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
def make_draft_asset_depr_schedules_if_not_present(asset_doc): def make_draft_asset_depr_schedules_if_not_present(asset_doc):

View File

@ -12,8 +12,7 @@
"column_break_3", "column_break_3",
"accumulated_depreciation_amount", "accumulated_depreciation_amount",
"journal_entry", "journal_entry",
"make_depreciation_entry", "make_depreciation_entry"
"depreciation_method"
], ],
"fields": [ "fields": [
{ {
@ -58,20 +57,11 @@
"fieldname": "make_depreciation_entry", "fieldname": "make_depreciation_entry",
"fieldtype": "Button", "fieldtype": "Button",
"label": "Make Depreciation Entry" "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, "istable": 1,
"links": [], "links": [],
"modified": "2022-12-06 20:35:50.264281", "modified": "2023-03-13 23:17:15.849950",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Depreciation Schedule", "name": "Depreciation Schedule",

View File

@ -16,6 +16,7 @@
"transaction_settings_section", "transaction_settings_section",
"po_required", "po_required",
"pr_required", "pr_required",
"over_order_allowance",
"column_break_12", "column_break_12",
"maintain_same_rate", "maintain_same_rate",
"set_landed_cost_based_on_purchase_invoice_rate", "set_landed_cost_based_on_purchase_invoice_rate",
@ -156,6 +157,13 @@
"fieldname": "set_landed_cost_based_on_purchase_invoice_rate", "fieldname": "set_landed_cost_based_on_purchase_invoice_rate",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Set Landed Cost Based on Purchase Invoice Rate" "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", "icon": "fa fa-cog",
@ -163,7 +171,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-02-28 15:41:32.686805", "modified": "2023-03-02 17:02:14.404622",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Buying Settings", "name": "Buying Settings",

View File

@ -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.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.buying.utils import check_on_hold_or_closed_status, validate_for_items
from erpnext.controllers.buying_controller import BuyingController 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.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.doctype.item.item import get_item_defaults, get_last_purchase_details
from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty 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_with_previous_doc()
self.validate_for_subcontracting() self.validate_for_subcontracting()
self.validate_minimum_order_qty() self.validate_minimum_order_qty()
validate_against_blanket_order(self)
if self.is_old_subcontracting_flow: if self.is_old_subcontracting_flow:
self.validate_bom_for_subcontracting_items() self.validate_bom_for_subcontracting_items()

View File

@ -113,7 +113,10 @@ class RequestforQuotation(BuyingController):
def get_link(self): def get_link(self):
# RFQ link for supplier portal # RFQ link for supplier portal
return get_url("/app/request-for-quotation/" + self.name) route = frappe.db.get_value(
"Portal Menu Item", {"reference_doctype": "Request for Quotation"}, ["route"]
)
return get_url("/app/{0}/".format(route) + self.name)
def update_supplier_part_no(self, supplier): def update_supplier_part_no(self, supplier):
self.vendor = supplier self.vendor = supplier

View File

@ -226,11 +226,11 @@ class TestWebsiteItem(unittest.TestCase):
self.assertTrue(bool(data.product_info["price"])) self.assertTrue(bool(data.product_info["price"]))
price_object = 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("price_list_rate"), 750)
self.assertEqual(price_object.get("formatted_mrp"), "₹ 1,000.00") 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_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 # switch to admin and disable show price
frappe.set_user("Administrator") frappe.set_user("Administrator")

View File

@ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', {
}, },
setup: function(frm) { 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("customer", "customer_name", "customer_name");
frm.add_fetch("supplier", "supplier_name", "supplier_name"); frm.add_fetch("supplier", "supplier_name", "supplier_name");
}, },

View File

@ -6,6 +6,7 @@ import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.query_builder.functions import Sum
from frappe.utils import flt, getdate from frappe.utils import flt, getdate
from erpnext.stock.doctype.item.item import get_item_defaults from erpnext.stock.doctype.item.item import get_item_defaults
@ -29,21 +30,23 @@ class BlanketOrder(Document):
def update_ordered_qty(self): def update_ordered_qty(self):
ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order" 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( item_ordered_qty = frappe._dict(
frappe.db.sql( (
""" frappe.qb.from_(trans_item)
select trans_item.item_code, sum(trans_item.stock_qty) as qty .from_(trans)
from `tab{0} Item` trans_item, `tab{0}` trans .select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty"))
where trans.name = trans_item.parent .where(
and trans_item.blanket_order=%s (trans.name == trans_item.parent)
and trans.docstatus=1 & (trans_item.blanket_order == self.name)
and trans.status not in ('Closed', 'Stopped') & (trans.docstatus == 1)
group by trans_item.item_code & (trans.status.notin(["Stopped", "Closed"]))
""".format( )
ref_doctype .groupby(trans_item.item_code)
), ).run()
self.name,
)
) )
for d in self.items: for d in self.items:
@ -79,7 +82,43 @@ def make_order(source_name):
"doctype": doctype + " Item", "doctype": doctype + " Item",
"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"}, "field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"},
"postprocess": update_item, "postprocess": update_item,
"condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0,
}, },
}, },
) )
return target_doc 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
)
)

View File

@ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase):
po1.currency = get_company_currency(po1.company) po1.currency = get_company_currency(po1.company)
self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) 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): def make_blanket_order(**args):
args = frappe._dict(args) args = frappe._dict(args)

View File

@ -31,7 +31,7 @@ class BOMTree:
# specifying the attributes to save resources # specifying the attributes to save resources
# ref: https://docs.python.org/3/reference/datamodel.html#slots # 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__( def __init__(
self, name: str, is_bom: bool = True, exploded_qty: float = 1.0, qty: float = 1 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): def __create_tree(self):
bom = frappe.get_cached_doc("BOM", self.name) bom = frappe.get_cached_doc("BOM", self.name)
self.item_code = bom.item self.item_code = bom.item
self.bom_qty = bom.quantity
for item in bom.get("items", []): 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 exploded_qty = self.exploded_qty * qty
if item.bom_no: if item.bom_no:
child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty) child = BOMTree(item.bom_no, exploded_qty=exploded_qty, qty=qty)

View File

@ -6,7 +6,7 @@ from collections import deque
from functools import partial from functools import partial
import frappe import frappe
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase, timeout
from frappe.utils import cstr, flt from frappe.utils import cstr, flt
from erpnext.controllers.tests.test_subcontracting_controller import ( from erpnext.controllers.tests.test_subcontracting_controller import (
@ -27,6 +27,7 @@ test_dependencies = ["Item", "Quality Inspection Template"]
class TestBOM(FrappeTestCase): class TestBOM(FrappeTestCase):
@timeout
def test_get_items(self): def test_get_items(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict 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.assertTrue(test_records[2]["items"][1]["item_code"] in items_dict)
self.assertEqual(len(items_dict.values()), 2) self.assertEqual(len(items_dict.values()), 2)
@timeout
def test_get_items_exploded(self): def test_get_items_exploded(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items_as_dict 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.assertTrue(test_records[0]["items"][1]["item_code"] in items_dict)
self.assertEqual(len(items_dict.values()), 3) self.assertEqual(len(items_dict.values()), 3)
@timeout
def test_get_items_list(self): def test_get_items_list(self):
from erpnext.manufacturing.doctype.bom.bom import get_bom_items from erpnext.manufacturing.doctype.bom.bom import get_bom_items
self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3) self.assertEqual(len(get_bom_items(bom=get_default_bom(), company="_Test Company")), 3)
@timeout
def test_default_bom(self): def test_default_bom(self):
def _get_default_bom_in_item(): def _get_default_bom_in_item():
return cstr(frappe.db.get_value("Item", "_Test FG Item 2", "default_bom")) 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) self.assertTrue(_get_default_bom_in_item(), bom.name)
@timeout
def test_update_bom_cost_in_all_boms(self): def test_update_bom_cost_in_all_boms(self):
# get current rate for '_Test Item 2' # get current rate for '_Test Item 2'
bom_rates = frappe.db.get_values( bom_rates = frappe.db.get_values(
@ -99,6 +104,7 @@ class TestBOM(FrappeTestCase):
): ):
self.assertEqual(d.base_rate, rm_base_rate + 10) self.assertEqual(d.base_rate, rm_base_rate + 10)
@timeout
def test_bom_cost(self): def test_bom_cost(self):
bom = frappe.copy_doc(test_records[2]) bom = frappe.copy_doc(test_records[2])
bom.insert() bom.insert()
@ -127,6 +133,7 @@ class TestBOM(FrappeTestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
@timeout
def test_bom_cost_with_batch_size(self): def test_bom_cost_with_batch_size(self):
bom = frappe.copy_doc(test_records[2]) bom = frappe.copy_doc(test_records[2])
bom.docstatus = 0 bom.docstatus = 0
@ -145,6 +152,7 @@ class TestBOM(FrappeTestCase):
self.assertAlmostEqual(bom.operating_cost, op_cost / 2) self.assertAlmostEqual(bom.operating_cost, op_cost / 2)
bom.delete() bom.delete()
@timeout
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): 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) 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)): 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_raw_material_cost, 27000)
self.assertEqual(bom.base_total_cost, 33000) self.assertEqual(bom.base_total_cost, 33000)
@timeout
def test_bom_cost_multi_uom_based_on_valuation_rate(self): def test_bom_cost_multi_uom_based_on_valuation_rate(self):
bom = frappe.copy_doc(test_records[2]) bom = frappe.copy_doc(test_records[2])
bom.set_rate_of_sub_assembly_item_based_on_bom = 0 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) self.assertEqual(bom.items[0].rate, 20)
@timeout
def test_bom_cost_with_fg_based_operating_cost(self): def test_bom_cost_with_fg_based_operating_cost(self):
bom = frappe.copy_doc(test_records[4]) bom = frappe.copy_doc(test_records[4])
bom.insert() bom.insert()
@ -229,6 +239,7 @@ class TestBOM(FrappeTestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
@timeout
def test_subcontractor_sourced_item(self): def test_subcontractor_sourced_item(self):
item_code = "_Test Subcontracted FG Item 1" item_code = "_Test Subcontracted FG Item 1"
set_backflush_based_on("Material Transferred for Subcontract") 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]) supplied_items = sorted([d.rm_item_code for d in sco.supplied_items])
self.assertEqual(bom_items, supplied_items) self.assertEqual(bom_items, supplied_items)
@timeout
def test_bom_tree_representation(self): def test_bom_tree_representation(self):
bom_tree = { bom_tree = {
"Assembly": { "Assembly": {
@ -335,6 +347,7 @@ class TestBOM(FrappeTestCase):
for reqd_item, created_item in zip(reqd_order, created_order): for reqd_item, created_item in zip(reqd_order, created_order):
self.assertEqual(reqd_item, created_item.item_code) self.assertEqual(reqd_item, created_item.item_code)
@timeout
def test_generated_variant_bom(self): def test_generated_variant_bom(self):
from erpnext.controllers.item_variant import create_variant 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.qty, created_item.qty)
self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty) self.assertEqual(reqd_item.exploded_qty, created_item.exploded_qty)
@timeout
def test_bom_recursion_1st_level(self): def test_bom_recursion_1st_level(self):
"""BOM should not allow BOM item again in child""" """BOM should not allow BOM item again in child"""
item_code = make_item(properties={"is_stock_item": 1}).name 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.items[0].bom_no = bom.name
bom.save() bom.save()
@timeout
def test_bom_recursion_transitive(self): def test_bom_recursion_transitive(self):
item1 = make_item(properties={"is_stock_item": 1}).name item1 = make_item(properties={"is_stock_item": 1}).name
item2 = 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() bom1.save()
bom2.save() bom2.save()
@timeout
def test_bom_with_process_loss_item(self): def test_bom_with_process_loss_item(self):
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items() 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 # Items with whole UOMs can't be PL Items
self.assertRaises(frappe.ValidationError, bom_doc.submit) self.assertRaises(frappe.ValidationError, bom_doc.submit)
@timeout
def test_bom_item_query(self): def test_bom_item_query(self):
query = partial( query = partial(
item_query, item_query,
@ -440,6 +457,7 @@ class TestBOM(FrappeTestCase):
) )
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results") self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
@timeout
def test_exclude_exploded_items_from_bom(self): def test_exclude_exploded_items_from_bom(self):
bom_no = get_default_bom() bom_no = get_default_bom()
new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no)) new_bom = frappe.copy_doc(frappe.get_doc("BOM", bom_no))
@ -458,6 +476,7 @@ class TestBOM(FrappeTestCase):
new_bom.delete() new_bom.delete()
@timeout
def test_valid_transfer_defaults(self): def test_valid_transfer_defaults(self):
bom_with_op = frappe.db.get_value( bom_with_op = frappe.db.get_value(
"BOM", {"item": "_Test FG Item 2", "with_operations": 1, "is_active": 1} "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") self.assertEqual(bom.transfer_material_against, "Work Order")
bom.delete() bom.delete()
@timeout
def test_bom_name_length(self): def test_bom_name_length(self):
"""test >140 char names""" """test >140 char names"""
bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}} bom_tree = {"x" * 140: {" ".join(["abc"] * 35): {}}}
create_nested_bom(bom_tree, prefix="") create_nested_bom(bom_tree, prefix="")
@timeout
def test_version_index(self): def test_version_index(self):
bom = frappe.new_doc("BOM") bom = frappe.new_doc("BOM")
@ -515,6 +536,7 @@ class TestBOM(FrappeTestCase):
msg=f"Incorrect index for {existing_boms}", msg=f"Incorrect index for {existing_boms}",
) )
@timeout
def test_bom_versioning(self): def test_bom_versioning(self):
bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}} bom_tree = {frappe.generate_hash(length=10): {frappe.generate_hash(length=10): {}}}
bom = create_nested_bom(bom_tree, prefix="") bom = create_nested_bom(bom_tree, prefix="")
@ -547,6 +569,7 @@ class TestBOM(FrappeTestCase):
self.assertNotEqual(amendment.name, version.name) self.assertNotEqual(amendment.name, version.name)
self.assertEqual(int(version.name.split("-")[-1]), 2) self.assertEqual(int(version.name.split("-")[-1]), 2)
@timeout
def test_clear_inpection_quality(self): def test_clear_inpection_quality(self):
bom = frappe.copy_doc(test_records[2], ignore_no_copy=True) 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) self.assertEqual(bom.quality_inspection_template, None)
@timeout
def test_bom_pricing_based_on_lpp(self): def test_bom_pricing_based_on_lpp(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@ -585,6 +609,7 @@ class TestBOM(FrappeTestCase):
bom.submit() bom.submit()
self.assertEqual(bom.items[0].rate, 42) self.assertEqual(bom.items[0].rate, 42)
@timeout
def test_set_default_bom_for_item_having_single_bom(self): def test_set_default_bom_for_item_having_single_bom(self):
from erpnext.stock.doctype.item.test_item import make_item from erpnext.stock.doctype.item.test_item import make_item
@ -621,6 +646,7 @@ class TestBOM(FrappeTestCase):
bom.reload() bom.reload()
self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name) self.assertEqual(frappe.get_value("Item", fg_item.item_code, "default_bom"), bom.name)
@timeout
def test_exploded_items_rate(self): def test_exploded_items_rate(self):
rm_item = make_item( rm_item = make_item(
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}
@ -649,6 +675,7 @@ class TestBOM(FrappeTestCase):
bom.submit() bom.submit()
self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate) self.assertEqual(bom.exploded_items[0].rate, bom.items[0].base_rate)
@timeout
def test_bom_cost_update_flag(self): def test_bom_cost_update_flag(self):
rm_item = make_item( rm_item = make_item(
properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89} properties={"is_stock_item": 1, "valuation_rate": 99, "last_purchase_rate": 89}

View File

@ -2,7 +2,7 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
import frappe 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 ( from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
update_cost_in_all_boms_in_test, update_cost_in_all_boms_in_test,
@ -20,6 +20,7 @@ class TestBOMUpdateTool(FrappeTestCase):
def tearDown(self): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()
@timeout
def test_replace_bom(self): def test_replace_bom(self):
current_bom = "BOM-_Test Item Home Desktop Manufactured-001" 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.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})) self.assertTrue(frappe.db.exists("BOM Item", {"bom_no": bom_doc.name, "docstatus": 1}))
@timeout
def test_bom_cost(self): def test_bom_cost(self):
for item in ["BOM Cost Test Item 1", "BOM Cost Test Item 2", "BOM Cost Test Item 3"]: 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) item_doc = create_item(item, valuation_rate=100)

View File

@ -682,7 +682,7 @@ class WorkOrder(Document):
for node in bom_traversal: for node in bom_traversal:
if node.is_bom: 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") bom_qty = frappe.get_cached_value("BOM", self.bom_no, "quantity")
operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty)) operations.extend(_get_operations(self.bom_no, qty=1.0 / bom_qty))

View File

@ -322,6 +322,8 @@ erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
erpnext.patches.v14_0.update_entry_type_for_journal_entry erpnext.patches.v14_0.update_entry_type_for_journal_entry
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
erpnext.patches.v14_0.set_pick_list_status 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_asset_value_for_manual_depr_entries
erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
# below migration patches should always run last # below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger

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

View File

@ -7,6 +7,9 @@ from erpnext.setup.utils import get_exchange_rate
def execute(): def execute():
frappe.reload_doc(
"accounts", "doctype", "currency_exchange_settings"
) # get_exchange_rate depends on Currency Exchange Settings
frappe.reload_doctype("Opportunity") frappe.reload_doctype("Opportunity")
opportunities = frappe.db.get_list( opportunities = frappe.db.get_list(
"Opportunity", "Opportunity",

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

View File

@ -5,6 +5,8 @@ frappe.ui.form.on("Timesheet", {
setup: function(frm) { setup: function(frm) {
frappe.require("/assets/erpnext/js/projects/timer.js"); frappe.require("/assets/erpnext/js/projects/timer.js");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice'];
frm.fields_dict.employee.get_query = function() { frm.fields_dict.employee.get_query = function() {
return { return {
filters:{ filters:{

View File

@ -46,6 +46,9 @@ def get_data(filters):
# task has no end date, hence no delay # task has no end date, hence no delay
task.delay = 0 task.delay = 0
task.status = _(task.status)
task.priority = _(task.priority)
# Sort by descending order of delay # Sort by descending order of delay
tasks.sort(key=lambda x: x["delay"], reverse=True) tasks.sort(key=lambda x: x["delay"], reverse=True)
return tasks return tasks
@ -73,7 +76,7 @@ def get_chart_data(data):
on_track = on_track + 1 on_track = on_track + 1
charts = { charts = {
"data": { "data": {
"labels": ["On Track", "Delayed"], "labels": [_("On Track"), _("Delayed")],
"datasets": [{"name": "Delayed", "values": [on_track, delay]}], "datasets": [{"name": "Delayed", "values": [on_track, delay]}],
}, },
"type": "percentage", "type": "percentage",

View File

@ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
) )
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.controllers.selling_controller import SellingController 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 ( from erpnext.manufacturing.doctype.production_plan.production_plan import (
get_items_for_material_requests, get_items_for_material_requests,
) )
@ -52,6 +55,7 @@ class SalesOrder(SellingController):
self.validate_warehouse() self.validate_warehouse()
self.validate_drop_ship() self.validate_drop_ship()
self.validate_serial_no_based_delivery() self.validate_serial_no_based_delivery()
validate_against_blanket_order(self)
validate_inter_company_party( validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_order_reference self.doctype, self.customer, self.company, self.inter_company_order_reference
) )

View File

@ -24,6 +24,7 @@
"so_required", "so_required",
"dn_required", "dn_required",
"sales_update_frequency", "sales_update_frequency",
"over_order_allowance",
"column_break_5", "column_break_5",
"allow_multiple_items", "allow_multiple_items",
"allow_against_multiple_purchase_orders", "allow_against_multiple_purchase_orders",
@ -179,6 +180,12 @@
"fieldname": "allow_sales_order_creation_for_expired_quotation", "fieldname": "allow_sales_order_creation_for_expired_quotation",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation" "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", "icon": "fa fa-cog",
@ -186,7 +193,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-02-04 12:37:53.380857", "modified": "2023-03-03 11:16:54.333615",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",

View File

@ -808,7 +808,7 @@ def get_default_company_address(name, sort_key="is_primary_address", existing_ad
return existing_address return existing_address
if out: 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: else:
return None return None

View File

@ -11,6 +11,7 @@ from frappe.utils import random_string
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import ( from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (
get_charts_for_country, 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_ignore = ["Account", "Cost Center", "Payment Terms Template", "Salary Component", "Warehouse"]
test_dependencies = ["Fiscal Year"] test_dependencies = ["Fiscal Year"]
@ -132,6 +133,38 @@ class TestCompany(unittest.TestCase):
self.assertTrue(lft >= min_lft) self.assertTrue(lft >= min_lft)
self.assertTrue(rgt <= max_rgt) 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(self, company):
def get_no_of_children(companies, no_of_children): def get_no_of_children(companies, no_of_children):
children = [] children = []

View File

@ -155,7 +155,7 @@ def add_standard_navbar_items():
{ {
"item_label": "Documentation", "item_label": "Documentation",
"item_type": "Route", "item_type": "Route",
"route": "https://erpnext.com/docs/user/manual", "route": "https://docs.erpnext.com/docs/v14/user/manual/en/introduction",
"is_standard": 1, "is_standard": 1,
}, },
{ {

View File

@ -636,7 +636,8 @@
"no_copy": 1, "no_copy": 1,
"options": "Sales Invoice", "options": "Sales Invoice",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"search_index": 1
}, },
{ {
"fieldname": "so_detail", "fieldname": "so_detail",
@ -837,7 +838,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2022-11-09 12:17:50.850142", "modified": "2023-03-20 14:24:10.406746",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note Item", "name": "Delivery Note Item",

View File

@ -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 not in options else item_barcode.barcode_type
) )
if 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 barcode_type in barcodenumber.barcodes():
if not barcodenumber.check_code(barcode_type, item_barcode.barcode): if not barcodenumber.check_code(barcode_type, item_barcode.barcode):
frappe.throw( 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 = { convert = {
"UPC-A": "UPCA", "UPC-A": "UPCA",
"CODE-39": "CODE39", "CODE-39": "CODE39",
"EAN": "EAN13",
"EAN-12": "EAN",
"EAN-8": "EAN8",
"ISBN-10": "ISBN10", "ISBN-10": "ISBN10",
"ISBN-13": "ISBN13", "ISBN-13": "ISBN13",
} }
if erpnext_number in convert: if erpnext_number in convert:
return convert[erpnext_number] return convert[erpnext_number]
else:
return erpnext_number return erpnext_number
def make_item_price(item, price_list_name, item_price): def make_item_price(item, price_list_name, item_price):

View File

@ -581,8 +581,9 @@ class TestItem(FrappeTestCase):
}, },
{"barcode": "72527273070", "barcode_type": "UPC-A"}, {"barcode": "72527273070", "barcode_type": "UPC-A"},
{"barcode": "123456", "barcode_type": "CODE-39"}, {"barcode": "123456", "barcode_type": "CODE-39"},
{"barcode": "401268452363", "barcode_type": "EAN-12"}, {"barcode": "401268452363", "barcode_type": "EAN"},
{"barcode": "90311017", "barcode_type": "EAN-8"}, {"barcode": "90311017", "barcode_type": "EAN"},
{"barcode": "73513537", "barcode_type": "EAN"},
{"barcode": "0123456789012", "barcode_type": "GS1"}, {"barcode": "0123456789012", "barcode_type": "GS1"},
{"barcode": "2211564566668", "barcode_type": "GTIN"}, {"barcode": "2211564566668", "barcode_type": "GTIN"},
{"barcode": "0256480249", "barcode_type": "ISBN"}, {"barcode": "0256480249", "barcode_type": "ISBN"},

View File

@ -172,8 +172,8 @@ class PickList(Document):
if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance: if (row.picked_qty / row.stock_qty) * 100 > over_delivery_receipt_allowance:
frappe.throw( 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() @frappe.whitelist()

View File

@ -658,6 +658,7 @@ class StockEntry(StockController):
) )
finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item) finished_item_qty = sum(d.transfer_qty for d in self.items if d.is_finished_item)
items = []
# Set basic rate for incoming items # Set basic rate for incoming items
for d in self.get("items"): for d in self.get("items"):
if d.s_warehouse or d.set_basic_rate_manually: if d.s_warehouse or d.set_basic_rate_manually:
@ -665,12 +666,7 @@ class StockEntry(StockController):
if d.allow_zero_valuation_rate: if d.allow_zero_valuation_rate:
d.basic_rate = 0.0 d.basic_rate = 0.0
frappe.msgprint( items.append(d.item_code)
_(
"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,
)
elif d.is_finished_item: elif d.is_finished_item:
if self.purpose == "Manufacture": if self.purpose == "Manufacture":
@ -697,6 +693,20 @@ class StockEntry(StockController):
d.basic_rate = flt(d.basic_rate) d.basic_rate = flt(d.basic_rate)
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount")) 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): def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
outgoing_items_cost = 0.0 outgoing_items_cost = 0.0
for d in self.get("items"): for d in self.get("items"):

View File

@ -2801,7 +2801,7 @@ Stock Ledger Entries and GL Entries are reposted for the selected Purchase Recei
Stock Levels,Niveaux du Stocks, Stock Levels,Niveaux du Stocks,
Stock Liabilities,Passif du Stock, Stock Liabilities,Passif du Stock,
Stock Options,Options du Stock, Stock Options,Options du Stock,
Stock Qty,Qté en Stock, Stock Qty,Qté en unité de stock,
Stock Received But Not Billed,Stock Reçus Mais Non Facturés, Stock Received But Not Billed,Stock Reçus Mais Non Facturés,
Stock Reports,Rapports de stock, Stock Reports,Rapports de stock,
Stock Summary,Résumé du Stock, Stock Summary,Résumé du Stock,

Can't render this file because it is too large.