Compare commits

..

6 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
3abc8da5b5 fix: correct rate_with_margin assertion in test_calculate_margin_amount_type
Agent-Logs-Url: https://github.com/frappe/erpnext/sessions/6c6729c0-87fc-407e-9ebf-862909068ffd

Co-authored-by: mihir-kandoi <8833206+mihir-kandoi@users.noreply.github.com>
2026-05-02 18:03:18 +00:00
copilot-swe-agent[bot]
0e807f3ef1 fix: replace frappe.get_doc with frappe.new_doc in new tests, fix RUF003 comment
Agent-Logs-Url: https://github.com/frappe/erpnext/sessions/b87be10b-3c3e-4f96-a102-a0206d727bd1

Co-authored-by: mihir-kandoi <8833206+mihir-kandoi@users.noreply.github.com>
2026-05-02 17:32:22 +00:00
copilot-swe-agent[bot]
252eef710c test: add coverage for taxes_and_totals and transaction_base
Agent-Logs-Url: https://github.com/frappe/erpnext/sessions/a54bcd34-9afc-47ca-b06d-a00df73a80ea

Co-authored-by: mihir-kandoi <8833206+mihir-kandoi@users.noreply.github.com>
2026-05-02 17:20:40 +00:00
Mihir Kandoi
032a282f84 ci: auto merge backports (#54701)
* ci: auto merge backports

* ci: add github action to propogate auto-merge label
2026-05-02 17:11:39 +00:00
mergify[bot]
ca093177e0 fix: set valid_from in created Item Price (backport #54696) (#54699)
* fix: set valid_from in created Item Price (#54696)

Co-authored-by: Kaajal-Chhattani <kaajal.chhattani@aurigait.com>
(cherry picked from commit 6246a9aa6e)

# Conflicts:
#	erpnext/stock/get_item_details.py

* chore: resolve conflicts

---------

Co-authored-by: Kaajalchhattani <89331214+Kaajalchhattani@users.noreply.github.com>
Co-authored-by: Mihir Kandoi <kandoimihir@gmail.com>
2026-05-02 16:29:16 +00:00
Kenneth Sequeira
ea3cf57042 fix: update frappe docker badge and link (#54702)
* fix: update frappe docker badge and link

* remove pwd link
2026-05-02 21:55:29 +05:30
6 changed files with 233 additions and 9 deletions

View File

@@ -0,0 +1,70 @@
name: Propagate auto-merge label to backports
on:
pull_request:
types: [opened]
jobs:
propagate-label:
runs-on: ubuntu-latest
permissions:
pull-requests: write
issues: write
steps:
- name: Detect backport and original PR (metadata)
id: extract
uses: actions/github-script@v7
with:
script: |
const pr = context.payload.pull_request;
const base = pr.base.ref;
if (!/^version-(15|16)-hotfix$/.test(base)) {
core.setOutput('is_backport', 'false');
return;
}
const isMergify = pr.user && pr.user.login === 'mergify[bot]';
const headRef = pr.head.ref || '';
const match = headRef.match(/(?:^|\/)pr-(\d+)(?:-|$)/i);
if (isMergify && match) {
const original = Number(match[1]);
if (!Number.isNaN(original)) {
core.setOutput('original_pr', String(original));
core.setOutput('is_backport', 'true');
return;
}
}
core.setOutput('is_backport', 'false');
- name: Check original PR labels
if: steps.extract.outputs.is_backport == 'true'
id: check
uses: actions/github-script@v7
with:
script: |
const prNumber = Number("${{ steps.extract.outputs.original_pr }}");
const { data } = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber
});
const labels = data.labels.map(l => l.name);
core.setOutput("has_auto_merge", labels.includes("auto-merge backports"));
- name: Add auto-merge label to backport PR
if: steps.check.outputs.has_auto_merge == 'true'
uses: actions/github-script@v7
with:
script: |
await github.rest.issues.addLabels({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.payload.pull_request.number,
labels: ["auto-merge"]
});

View File

@@ -92,3 +92,18 @@ pull_request_rules:
{{ title }} (#{{ number }})
{{ body }}
- name: Auto merge backports (safe)
conditions:
- label=auto-merge
- base~=^version-(15|16)-hotfix$
- -conflict
- status-success=Python Unit Tests (1)
- status-success=Python Unit Tests (2)
- status-success=Python Unit Tests (3)
- status-success=Python Unit Tests (4)
- status-success=linters
- status-success=semgrep
- status-success=Patch Test
actions:
merge:
method: squash

View File

@@ -10,7 +10,7 @@
[![Learn on Frappe School](https://img.shields.io/badge/Frappe%20School-Learn%20ERPNext-blue?style=flat-square)](https://frappe.school)<br><br>
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml/badge.svg?event=schedule)](https://github.com/frappe/erpnext/actions/workflows/server-tests-mariadb.yml)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext-worker.svg)](https://hub.docker.com/r/frappe/erpnext-worker)
[![docker pulls](https://img.shields.io/docker/pulls/frappe/erpnext.svg)](https://hub.docker.com/r/frappe/erpnext)
</div>
@@ -88,14 +88,6 @@ See [Frappe Docker Documentation](https://github.com/frappe/frappe_docker) for f
> For Docker basics and best practices refer to Docker's [documentation](https://docs.docker.com)
#### Demo setup
The fastest way to try ERPNext is to play in a pre-configured sandbox, in your browser, click the button below:
<a href="https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/frappe/frappe_docker/main/pwd.yml">
<img src="https://raw.githubusercontent.com/play-with-docker/stacks/master/assets/images/button.png" alt="Try in PWD"/>
</a>
### Try on your environment
> **⚠️ Disposable demo only**

View File

@@ -3,6 +3,7 @@ from unittest.mock import patch
import frappe
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
from erpnext.selling.doctype.quotation.test_quotation import make_quotation
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.tests.utils import ERPNextTestSuite
@@ -59,3 +60,82 @@ class TestTaxesAndTotals(ERPNextTestSuite):
self.assertEqual(so.rounding_adjustment, 0)
self.assertEqual(so.base_rounded_total, 0)
self.assertEqual(so.base_rounding_adjustment, 0)
def test_calculate_margin_amount_type(self):
"""When rate exceeds price_list_rate and no pricing rules, margin type is set to 'Amount'."""
so = make_sales_order(do_not_save=True)
item = so.items[0]
item.qty = 2
item.price_list_rate = 100.0
item.rate = 120.0 # rate > price_list_rate -> implicit Amount margin
item.pricing_rules = ""
item.margin_type = None
item.margin_rate_or_amount = 0
calculate_taxes_and_totals(so)
self.assertEqual(item.margin_type, "Amount")
self.assertEqual(item.margin_rate_or_amount, 20.0)
# The implicit-Amount branch does not populate rate_with_margin; the rate is preserved.
self.assertEqual(item.rate, 120.0)
def test_calculate_margin_percentage_type(self):
"""Percentage margin should add a fraction of price_list_rate to derive rate_with_margin."""
so = make_sales_order(do_not_save=True)
item = so.items[0]
item.qty = 1
item.price_list_rate = 200.0
item.rate = 200.0
item.pricing_rules = ""
item.margin_type = "Percentage"
item.margin_rate_or_amount = 10 # 10% margin
calculate_taxes_and_totals(so)
# rate_with_margin = price_list_rate * (1 + margin_rate / 100)
expected_rate_with_margin = 200.0 * 1.10
self.assertAlmostEqual(item.rate_with_margin, expected_rate_with_margin, places=2)
def test_filter_rows_excludes_alternative_items(self):
"""Quotation totals must not include rows marked as is_alternative."""
qo = make_quotation(qty=5, rate=100, do_not_save=True)
# Append an alternative item that should be excluded from the net total
qo.append(
"items",
{
"item_code": "_Test Item",
"warehouse": "_Test Warehouse - _TC",
"qty": 10,
"rate": 500,
"is_alternative": 1,
},
)
calculate_taxes_and_totals(qo)
# Only the first (non-alternative) item should contribute: 5 x 100 = 500
self.assertEqual(qo.net_total, 500.0)
self.assertEqual(qo.grand_total, 500.0)
def test_calculate_total_net_weight(self):
"""total_net_weight must equal the sum of total_weight across all item rows."""
so = make_sales_order(do_not_save=True)
so.items[0].qty = 3
so.items[0].rate = 50
so.items[0].total_weight = 6.0 # set directly so no item master lookup needed
calculate_taxes_and_totals(so)
self.assertEqual(so.total_net_weight, 6.0)
def test_set_discount_amount_exceeds_grand_total_throws(self):
"""Discount amount larger than grand total must raise a ValidationError."""
so = make_sales_order(do_not_save=True)
so.items[0].qty = 1
so.items[0].rate = 100
so.apply_discount_on = "Grand Total"
so.discount_amount = 200 # more than the 100 grand total
# _action must be set to trigger the validation path
so._action = "save"
self.assertRaises(frappe.ValidationError, calculate_taxes_and_totals, so)

View File

@@ -1,6 +1,7 @@
import frappe
from erpnext.tests.utils import ERPNextTestSuite
from erpnext.utilities.transaction_base import validate_uom_is_integer
class TestUtils(ERPNextTestSuite):
@@ -92,3 +93,67 @@ class TestUtils(ERPNextTestSuite):
doc.reset_default_field_value("to_warehouse", "items", "t_warehouse")
self.assertEqual(doc.from_warehouse, None)
self.assertEqual(doc.to_warehouse, "Warehouse 2")
def test_validate_posting_time_invalid(self):
"""An invalid posting_time string must raise a ValidationError."""
doc = frappe.new_doc("Stock Entry")
doc.set_posting_time = 1
doc.posting_time = "not-a-time"
self.assertRaises(frappe.ValidationError, doc.validate_posting_time)
def test_validate_posting_time_auto_set(self):
"""When set_posting_time is falsy, posting_date and posting_time are replaced with now."""
from frappe.utils import nowdate
doc = frappe.new_doc("Stock Entry")
doc.set_posting_time = 0
doc.posting_date = "2000-01-01"
doc.posting_time = "00:00:00"
doc.validate_posting_time()
# Both fields must have been refreshed to the current date/time
self.assertEqual(doc.posting_date, nowdate())
# posting_time should look like HH:MM:SS (not the old midnight value)
self.assertNotEqual(doc.posting_time, "00:00:00")
def test_validate_uom_is_integer_raises_for_fraction(self):
"""Fractional qty in a whole-number UOM must raise UOMMustBeIntegerError."""
from erpnext.utilities.transaction_base import UOMMustBeIntegerError
# Nos is seeded as a whole-number UOM in test fixtures
se = frappe.new_doc("Stock Entry")
se.purpose = "Material Receipt"
se.company = "_Test Company"
se.append(
"items",
{
"item_code": "_Test Item",
"uom": "Nos",
"qty": 1.5,
"t_warehouse": "_Test Warehouse - _TC",
"basic_rate": 100,
},
)
self.assertRaises(UOMMustBeIntegerError, validate_uom_is_integer, se, "uom", "qty")
def test_validate_uom_is_integer_passes_for_whole_number(self):
"""Integer qty in a whole-number UOM must NOT raise any error."""
se = frappe.new_doc("Stock Entry")
se.purpose = "Material Receipt"
se.company = "_Test Company"
se.append(
"items",
{
"item_code": "_Test Item",
"uom": "Nos",
"qty": 3,
"t_warehouse": "_Test Warehouse - _TC",
"basic_rate": 100,
},
)
# Should complete without raising
validate_uom_is_integer(se, "uom", "qty")

View File

@@ -1134,6 +1134,7 @@ def insert_item_price(ctx: ItemDetailsCtx):
currency=ctx.currency,
uom=ctx.stock_uom,
price_list=ctx.price_list,
valid_from=transaction_date,
)
item_price.insert()
frappe.msgprint(
@@ -1156,6 +1157,7 @@ def insert_item_price(ctx: ItemDetailsCtx):
"currency": ctx.currency,
"price_list_rate": price_list_rate,
"uom": ctx.stock_uom,
"valid_from": transaction_date,
}
)
item_price.insert()