diff --git a/.github/workflows/docs-checker.yml b/.github/workflows/docs-checker.yml index b644568d5ea..722c1252ed9 100644 --- a/.github/workflows/docs-checker.yml +++ b/.github/workflows/docs-checker.yml @@ -12,7 +12,7 @@ jobs: - name: 'Setup Environment' uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.10' - name: 'Clone repo' uses: actions/checkout@v2 diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml index 2cf44444cc3..a71db7289ae 100644 --- a/.github/workflows/patch.yml +++ b/.github/workflows/patch.yml @@ -35,9 +35,9 @@ jobs: uses: actions/checkout@v2 - name: Setup Python - uses: actions/setup-python@v2 + uses: "gabrielfalcao/pyenv-action@v9" with: - python-version: 3.8 + versions: 3.10:latest, 3.7:latest - name: Setup Node uses: actions/setup-node@v2 @@ -52,7 +52,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- @@ -82,7 +82,10 @@ jobs: ${{ runner.os }}-yarn- - name: Install - run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh + run: | + pip install frappe-bench + pyenv global $(pyenv versions | grep '3.10') + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh env: DB: mariadb TYPE: server @@ -96,18 +99,23 @@ jobs: git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git + pyenv global $(pyenv versions | grep '3.7') for version in $(seq 12 13) do echo "Updating to v$version" branch_name="version-$version-hotfix" + git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name git -C "apps/frappe" checkout -q -f $branch_name git -C "apps/erpnext" checkout -q -f $branch_name - bench setup requirements --python + rm -rf ~/frappe-bench/env + bench setup env + bench pip install -e ./apps/erpnext + bench --site test_site migrate done @@ -115,5 +123,10 @@ jobs: echo "Updating to latest version" git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" - bench setup requirements --python + + pyenv global $(pyenv versions | grep '3.10') + rm -rf ~/frappe-bench/env + bench -v setup env + bench pip install -e ./apps/erpnext + bench --site test_site migrate diff --git a/.github/workflows/server-tests-mariadb.yml b/.github/workflows/server-tests-mariadb.yml index cdb68499ffd..f65cb460f19 100644 --- a/.github/workflows/server-tests-mariadb.yml +++ b/.github/workflows/server-tests-mariadb.yml @@ -39,7 +39,7 @@ jobs: fail-fast: false matrix: - container: [1, 2, 3] + container: [1, 2, 3, 4] name: Python Unit Tests @@ -59,7 +59,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.10' - name: Setup Node uses: actions/setup-node@v2 @@ -74,7 +74,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- diff --git a/.github/workflows/server-tests-postgres.yml b/.github/workflows/server-tests-postgres.yml index 77d3c1ae61b..53a94dbfacb 100644 --- a/.github/workflows/server-tests-postgres.yml +++ b/.github/workflows/server-tests-postgres.yml @@ -21,7 +21,7 @@ jobs: strategy: fail-fast: false matrix: - container: [1, 2, 3] + container: [1] name: Python Unit Tests @@ -46,7 +46,7 @@ jobs: - name: Setup Python uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: '3.10' - name: Setup Node uses: actions/setup-node@v2 @@ -61,7 +61,7 @@ jobs: uses: actions/cache@v2 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- diff --git a/CODEOWNERS b/CODEOWNERS index bfc26010882..ecbae86d96b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -3,33 +3,30 @@ # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, -erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 -erpnext/assets/ @nextchamp-saqib @deepeshgarg007 -erpnext/erpnext_integrations/ @nextchamp-saqib +erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar +erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 -erpnext/regional @nextchamp-saqib @deepeshgarg007 -erpnext/selling @nextchamp-saqib @deepeshgarg007 +erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar +erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/support/ @nextchamp-saqib @deepeshgarg007 pos* @nextchamp-saqib -erpnext/buying/ @marination @rohitwaghchaure @ankush +erpnext/buying/ @marination @rohitwaghchaure @s-aga-r erpnext/e_commerce/ @marination -erpnext/maintenance/ @marination @rohitwaghchaure -erpnext/manufacturing/ @marination @rohitwaghchaure @ankush +erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r +erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r erpnext/portal/ @marination -erpnext/quality_management/ @marination @rohitwaghchaure +erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r erpnext/shopping_cart/ @marination -erpnext/stock/ @marination @rohitwaghchaure @ankush +erpnext/stock/ @marination @rohitwaghchaure @s-aga-r -erpnext/crm/ @ruchamahabal @pateljannat -erpnext/education/ @ruchamahabal @pateljannat -erpnext/hr/ @ruchamahabal @pateljannat -erpnext/payroll @ruchamahabal @pateljannat -erpnext/projects/ @ruchamahabal @pateljannat +erpnext/crm/ @NagariaHussain +erpnext/education/ @rutwikhdev +erpnext/projects/ @ruchamahabal -erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush -erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush +erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination +erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination erpnext/public/ @nextchamp-saqib @marination .github/ @ankush -requirements.txt @gavindsouza +pyproject.toml @gavindsouza @ankush diff --git a/dev-requirements.txt b/dev-requirements.txt deleted file mode 100644 index 15545c0efa2..00000000000 --- a/dev-requirements.txt +++ /dev/null @@ -1 +0,0 @@ -hypothesis~=6.31.0 diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py index 5b2b526e591..5ed34d34a32 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py @@ -162,7 +162,7 @@ class PaymentReconciliation(Document): { "reference_type": inv.voucher_type, "reference_name": inv.voucher_no, - "amount": -(inv.outstanding), + "amount": -(inv.outstanding_in_account_currency), "posting_date": inv.posting_date, "currency": inv.currency, } diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py index 575ac74a4eb..325346d6b6e 100644 --- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py +++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py @@ -19,6 +19,7 @@ class TestPaymentReconciliation(FrappeTestCase): self.create_company() self.create_item() self.create_customer() + self.create_account() self.clear_old_entries() def tearDown(self): @@ -89,6 +90,38 @@ class TestPaymentReconciliation(FrappeTestCase): customer.save() self.customer2 = customer.name + if frappe.db.exists("Customer", "_Test PR Customer 3"): + self.customer3 = "_Test PR Customer 3" + else: + customer = frappe.new_doc("Customer") + customer.customer_name = "_Test PR Customer 3" + customer.type = "Individual" + customer.default_currency = "EUR" + customer.save() + self.customer3 = customer.name + + def create_account(self): + account_name = "Debtors EUR" + if not frappe.db.get_value( + "Account", filters={"account_name": account_name, "company": self.company} + ): + acc = frappe.new_doc("Account") + acc.account_name = account_name + acc.parent_account = "Accounts Receivable - _PR" + acc.company = self.company + acc.account_currency = "EUR" + acc.account_type = "Receivable" + acc.insert() + else: + name = frappe.db.get_value( + "Account", + filters={"account_name": account_name, "company": self.company}, + fieldname="name", + pluck=True, + ) + acc = frappe.get_doc("Account", name) + self.debtors_eur = acc.name + def create_sales_invoice( self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False ): @@ -454,3 +487,56 @@ class TestPaymentReconciliation(FrappeTestCase): self.assertEqual(len(pr.get("payments")), 1) self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20) self.assertEqual(pr.get("payments")[0].amount, 20) + + def test_pr_output_foreign_currency_and_amount(self): + # test for currency and amount invoices and payments + transaction_date = nowdate() + # In EUR + amount = 100 + exchange_rate = 80 + + si = self.create_sales_invoice( + qty=1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + si.customer = self.customer3 + si.currency = "EUR" + si.conversion_rate = exchange_rate + si.debit_to = self.debtors_eur + si = si.save().submit() + + cr_note = self.create_sales_invoice( + qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True + ) + cr_note.customer = self.customer3 + cr_note.is_return = 1 + cr_note.currency = "EUR" + cr_note.conversion_rate = exchange_rate + cr_note.debit_to = self.debtors_eur + cr_note = cr_note.save().submit() + + pr = self.create_payment_reconciliation() + pr.party = self.customer3 + pr.receivable_payable_account = self.debtors_eur + pr.get_unreconciled_entries() + + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + + self.assertEqual(pr.invoices[0].amount, amount) + self.assertEqual(pr.invoices[0].currency, "EUR") + self.assertEqual(pr.payments[0].amount, amount) + self.assertEqual(pr.payments[0].currency, "EUR") + + cr_note.cancel() + + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + pay = get_payment_entry(si.doctype, si.name) + pay.references.clear() + pay = pay.save().submit() + + pr.get_unreconciled_entries() + self.assertEqual(len(pr.invoices), 1) + self.assertEqual(len(pr.payments), 1) + self.assertEqual(pr.payments[0].amount, amount) + self.assertEqual(pr.payments[0].currency, "EUR") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index aefa9a59ddf..cdb187f2185 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -476,6 +476,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e this.frm.trigger("calculate_timesheet_totals"); } } + + is_cash_or_non_trade_discount() { + this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount); + if (!this.frm.doc.is_cash_or_non_trade_discount) { + this.frm.set_value("additional_discount_account", ""); + } + } }; // for backward compatibility: combine new and previous states diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 327545aa54e..499377d4263 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -106,6 +106,7 @@ "loyalty_redemption_cost_center", "section_break_49", "apply_discount_on", + "is_cash_or_non_trade_discount", "base_discount_amount", "additional_discount_account", "column_break_51", @@ -1790,8 +1791,6 @@ "width": "50%" }, { - "fetch_from": "sales_partner.commission_rate", - "fetch_if_empty": 1, "fieldname": "commission_rate", "fieldtype": "Float", "hide_days": 1, @@ -1990,7 +1989,7 @@ { "fieldname": "additional_discount_account", "fieldtype": "Link", - "label": "Additional Discount Account", + "label": "Discount Account", "options": "Account" }, { @@ -2028,6 +2027,13 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.apply_discount_on == \"Grand Total\"", + "fieldname": "is_cash_or_non_trade_discount", + "fieldtype": "Check", + "label": "Is Cash or Non Trade Discount" } ], "icon": "fa fa-file-text", @@ -2040,7 +2046,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2022-06-10 03:52:51.409913", + "modified": "2022-06-16 16:22:44.870575", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 1a3164b0d94..6530e5ce860 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1030,7 +1030,7 @@ class SalesInvoice(SellingController): ) if grand_total and not self.is_internal_transfer(): - # Didnot use base_grand_total to book rounding loss gle + # Did not use base_grand_total to book rounding loss gle gl_entries.append( self.get_gl_dict( { @@ -1055,6 +1055,22 @@ class SalesInvoice(SellingController): ) ) + if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"): + gl_entries.append( + self.get_gl_dict( + { + "account": self.additional_discount_account, + "against": self.debit_to, + "debit": self.base_discount_amount, + "debit_in_account_currency": self.discount_amount, + "cost_center": self.cost_center, + "project": self.project, + }, + self.currency, + item=self, + ) + ) + def make_tax_gl_entries(self, gl_entries): enable_discount_accounting = cint( frappe.db.get_single_value("Selling Settings", "enable_discount_accounting") @@ -2117,6 +2133,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): source_document_warehouse_field = "from_warehouse" target_document_warehouse_field = "target_warehouse" + received_items = get_received_items(source_name, target_doctype, target_detail_field) + validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -2181,12 +2199,17 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): shipping_address_name=target_doc.shipping_address_name, ) + def update_item(source, target, source_parent): + target.qty = flt(source.qty) - received_items.get(source.name, 0.0) + item_field_map = { "doctype": target_doctype + " Item", "field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"], "field_map": { "rate": "rate", }, + "postprocess": update_item, + "condition": lambda doc: doc.qty > 0, } if doctype in ["Sales Invoice", "Sales Order"]: @@ -2224,6 +2247,28 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): return doclist +def get_received_items(reference_name, doctype, reference_fieldname): + target_doctypes = frappe.get_all( + doctype, + filters={"inter_company_invoice_reference": reference_name, "docstatus": 1}, + as_list=True, + ) + + if target_doctypes: + target_doctypes = list(target_doctypes[0]) + + received_items_map = frappe._dict( + frappe.get_all( + doctype + " Item", + filters={"parent": ("in", target_doctypes)}, + fields=[reference_fieldname, "qty"], + as_list=1, + ) + ) + + return received_items_map + + def set_purchase_references(doc): # add internal PO or PR links if any if doc.is_internal_transfer(): diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index c0005f78cfd..0a765f3f46f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -11,6 +11,7 @@ def get_data(): "Payment Request": "reference_name", "Sales Invoice": "return_against", "Auto Repeat": "reference_document", + "Purchase Invoice": "inter_company_invoice_reference", }, "internal_links": { "Sales Order": ["items", "sales_order"], @@ -30,5 +31,6 @@ def get_data(): {"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]}, {"label": _("Returns"), "items": ["Sales Invoice"]}, {"label": _("Subscription"), "items": ["Auto Repeat"]}, + {"label": _("Internal Transfers"), "items": ["Purchase Invoice"]}, ], } diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 448ec54b1ee..c2e82fb214d 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -2731,6 +2731,63 @@ class TestSalesInvoice(unittest.TestCase): einvoice = make_einvoice(si) validate_totals(einvoice) + def test_einvoice_discounts(self): + from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals + + # Normal Itemized Discount + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "" + si.items[0].discount_amount = 4000 + si.items[1].discount_amount = 300 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 4000) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 300) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + # Invoice Discount on net total + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Net Total" + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 316.83) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 83.17) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + # Invoice Discount on grand total (Itemized Discount) + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Grand Total" + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 268.5) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 70.48) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + # Invoice Discount on grand total (Cash/Non-Trade Discount) + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Grand Total" + si.is_cash_or_non_trade_discount = 1 + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ValDtls"]["Discount"], 400) + def test_item_tax_net_range(self): item = create_item("T Shirt") diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 8daff9d1936..6be8fd7e60b 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -855,8 +855,8 @@ def get_outstanding_invoices( ) for d in invoice_list: - payment_amount = d.invoice_amount - d.outstanding - outstanding_amount = d.outstanding + payment_amount = d.invoice_amount_in_account_currency - d.outstanding_in_account_currency + outstanding_amount = d.outstanding_in_account_currency if outstanding_amount > 0.5 / (10**precision): if ( min_outstanding @@ -872,7 +872,7 @@ def get_outstanding_invoices( "voucher_no": d.voucher_no, "voucher_type": d.voucher_type, "posting_date": d.posting_date, - "invoice_amount": flt(d.invoice_amount), + "invoice_amount": flt(d.invoice_amount_in_account_currency), "payment_amount": payment_amount, "outstanding_amount": outstanding_amount, "due_date": d.due_date, @@ -1412,7 +1412,7 @@ def create_payment_ledger_entry( if gle.against_voucher_type else gle.voucher_type, "against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no, - "currency": gle.currency, + "account_currency": gle.account_currency, "amount": dr_or_cr, "amount_in_account_currency": dr_or_cr_account_currency, "delinked": True if cancel else False, diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index 2144055b343..0d8cffe03f8 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -500,6 +500,9 @@ class calculate_taxes_and_totals(object): else: self.doc.grand_total = flt(self.doc.net_total) + if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"): + self.doc.grand_total -= self.doc.discount_amount + if self.doc.get("taxes"): self.doc.total_taxes_and_charges = flt( self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment), @@ -594,6 +597,12 @@ class calculate_taxes_and_totals(object): if not self.doc.apply_discount_on: frappe.throw(_("Please select Apply Discount On")) + if self.doc.apply_discount_on == "Grand Total" and self.doc.get( + "is_cash_or_non_trade_discount" + ): + self.discount_amount_applied = True + return + self.doc.base_discount_amount = flt( self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount") ) diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py index e1ccc117e75..7b7d0571b68 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -624,7 +624,7 @@ class SalarySlip(TransactionBase): data = self.get_data_for_eval() for struct_row in self._salary_structure_doc.get(component_type): amount = self.eval_condition_and_formula(struct_row, data) - if amount and struct_row.statistical_component == 0: + if amount is not None and struct_row.statistical_component == 0: self.update_component_row(struct_row, amount, component_type) def get_data_for_eval(self): @@ -854,6 +854,10 @@ class SalarySlip(TransactionBase): component_row, joining_date, relieving_date )[0] + # remove 0 valued components that have been updated later + if component_row.amount == 0: + self.remove(component_row) + def set_precision_for_component_amounts(self): for component_type in ("earnings", "deductions"): for component_row in self.get(component_type): diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 01f72adf349..62b98ec5bff 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1107,9 +1107,25 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } } + is_a_mapped_document(item) { + const mapped_item_field_map = { + "Delivery Note Item": ["si_detail", "so_detail", "dn_detail"], + "Sales Invoice Item": ["dn_detail", "so_detail", "sales_invoice_item"], + "Purchase Receipt Item": ["purchase_order_item", "purchase_invoice_item", "purchase_receipt_item"], + "Purchase Invoice Item": ["purchase_order_item", "pr_detail", "po_detail"], + }; + const mappped_fields = mapped_item_field_map[item.doctype] || []; + + return mappped_fields + .map((field) => item[field]) + .filter(Boolean).length > 0; + } + batch_no(doc, cdt, cdn) { let item = frappe.get_doc(cdt, cdn); - this.apply_price_list(item, true); + if (!this.is_a_mapped_document(item)) { + this.apply_price_list(item, true); + } } toggle_conversion_factor(item) { @@ -1483,48 +1499,46 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe } _set_values_for_item_list(children) { - var me = this; - var items_rule_dict = {}; + const items_rule_dict = {}; - for(var i=0, l=children.length; i 0) { - me.apply_product_discount(d); + if (child.free_item_data.length > 0) { + this.apply_product_discount(child); } - if (d.apply_rule_on_other_items) { - items_rule_dict[d.name] = d; + if (child.apply_rule_on_other_items) { + items_rule_dict[child.name] = child; } } - me.frm.refresh_field('items'); - me.apply_rule_on_other_items(items_rule_dict); - - me.calculate_taxes_and_totals(); + this.apply_rule_on_other_items(items_rule_dict); + this.calculate_taxes_and_totals(); } apply_rule_on_other_items(args) { diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py index 5eb14a5ddd3..fb2779bb481 100644 --- a/erpnext/regional/india/e_invoice/utils.py +++ b/erpnext/regional/india/e_invoice/utils.py @@ -271,14 +271,14 @@ def get_item_list(invoice): item.description = sanitize_for_json(d.item_name) item.qty = abs(item.qty) - if flt(item.qty) != 0.0: - item.unit_rate = abs(item.taxable_value / item.qty) - else: - item.unit_rate = abs(item.taxable_value) - item.gross_amount = abs(item.taxable_value) - item.taxable_value = abs(item.taxable_value) - item.discount_amount = 0 + if invoice.get("apply_discount_on"): + item.discount_amount = item.base_amount - item.base_net_amount + + item.unit_rate = abs(item.taxable_value - item.discount_amount) / item.qty + + item.gross_amount = abs(item.taxable_value) + item.discount_amount + item.taxable_value = abs(item.taxable_value) item.is_service_item = "Y" if item.gst_hsn_code and item.gst_hsn_code[:2] == "99" else "N" item.serial_no = "" @@ -352,7 +352,14 @@ def update_item_taxes(invoice, item): def get_invoice_value_details(invoice): invoice_value_details = frappe._dict(dict()) invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get("items")])) - invoice_value_details.invoice_discount_amt = 0 + if ( + invoice.apply_discount_on == "Grand Total" + and invoice.discount_amount + and invoice.get("is_cash_or_non_trade_discount") + ): + invoice_value_details.invoice_discount_amt = invoice.discount_amount + else: + invoice_value_details.invoice_discount_amt = 0 invoice_value_details.round_off = invoice.base_rounding_adjustment invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs( diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 02624695710..f1586fc3dc1 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -1060,8 +1060,16 @@ def update_taxable_values(doc, method): considered_rows.append(prev_row_id) for item in doc.get("items"): - proportionate_value = item.base_net_amount if doc.base_net_total else item.qty - total_value = doc.base_net_total if doc.base_net_total else doc.total_qty + if ( + doc.apply_discount_on == "Grand Total" + and doc.discount_amount + and doc.get("is_cash_or_non_trade_discount") + ): + proportionate_value = item.base_amount if doc.base_total else item.qty + total_value = doc.base_total if doc.base_total else doc.total_qty + else: + proportionate_value = item.base_net_amount if doc.base_net_total else item.qty + total_value = doc.base_net_total if doc.base_net_total else doc.total_qty applicable_charges = flt( flt( diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 0de5b2d5a32..3dec303a926 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -219,8 +219,8 @@ erpnext.company.setup_queries = function(frm) { ["default_discount_account", {}], ["discount_allowed_account", {"root_type": "Expense"}], ["discount_received_account", {"root_type": "Income"}], - ["exchange_gain_loss_account", {"root_type": "Expense"}], - ["unrealized_exchange_gain_loss_account", {"root_type": "Expense"}], + ["exchange_gain_loss_account", {"root_type": ["in", ["Expense", "Income"]]}], + ["unrealized_exchange_gain_loss_account", {"root_type": ["in", ["Expense", "Income"]]}], ["accumulated_depreciation_account", {"root_type": "Asset", "account_type": "Accumulated Depreciation"}], ["depreciation_expense_account", {"root_type": "Expense", "account_type": "Depreciation"}], diff --git a/pyproject.toml b/pyproject.toml index 8043dd99065..5acfd392726 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,35 @@ +[project] +name = "erpnext" +authors = [ + { name = "Frappe Technologies Pvt Ltd", email = "developers@frappe.io"} +] +description = "Open Source ERP" +requires-python = ">=3.10" +readme = "README.md" +dynamic = ["version"] +dependencies = [ + # Core dependencies + "pycountry~=20.7.3", + "python-stdnum~=1.16", + "Unidecode~=1.2.0", + "redisearch~=2.1.0", + + # integration dependencies + "gocardless-pro~=1.22.0", + "googlemaps", + "plaid-python~=7.2.1", + "python-youtube~=0.8.0", + "taxjar~=1.9.2", + "tweepy~=3.10.0", +] + +[build-system] +requires = ["flit_core >=3.4,<4"] +build-backend = "flit_core.buildapi" + +[tool.bench.dev-dependencies] +hypothesis = "~=6.31.0" + [tool.black] line-length = 99 diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 83e53758be2..00000000000 --- a/requirements.txt +++ /dev/null @@ -1,11 +0,0 @@ -# frappe # https://github.com/frappe/frappe is installed during bench-init -gocardless-pro~=1.22.0 -googlemaps -plaid-python~=7.2.1 -pycountry~=20.7.3 -python-stdnum~=1.16 -python-youtube~=0.8.0 -taxjar~=1.9.2 -tweepy~=3.10.0 -Unidecode~=1.2.0 -redisearch~=2.1.0 diff --git a/setup.py b/setup.py index 1faff0412f8..0ea4d073488 100644 --- a/setup.py +++ b/setup.py @@ -1,23 +1,6 @@ -from setuptools import setup, find_packages -import re, ast +# TODO: Remove this file when v15.0.0 is released +from setuptools import setup -# get version from __version__ variable in erpnext/__init__.py -_version_re = re.compile(r"__version__\s+=\s+(.*)") +name = "frappe" -with open("requirements.txt") as f: - install_requires = f.read().strip().split("\n") - -with open("erpnext/__init__.py", "rb") as f: - version = str(ast.literal_eval(_version_re.search(f.read().decode("utf-8")).group(1))) - -setup( - name="erpnext", - version=version, - description="Open Source ERP", - author="Frappe Technologies", - author_email="info@erpnext.com", - packages=find_packages(), - zip_safe=False, - include_package_data=True, - install_requires=install_requires, -) +setup()